derivedStateOf dependency tracking under concurrent state mutations
0 reputation · 22 Jun 2020, 05:55 UTC
0 reputation · 22 Jun 2020, 05:55 UTC
Determine whether derivedStateOf can skip recalculation when multiple coroutines mutate the same source state between composition frames in Jetpack Compose.
derivedStateOf tracks state reads during its calculation block and recomputes only when a tracked state object changes. Under concurrent writes, Compose may skip intermediate values if the final state is reached before the next frame begins. It is not documented whether derivedStateOf treats a collapsed concurrent update as one dependency change or as multiple changes that force repeated recalculation within a single frame. This matters for long lists where derived values control item visibility or measurement.
34775 reputation · 22 Jun 2020, 11:17 UTC
1. Recomputation per frame: When several coroutines mutate the same source State object inside a single frame, Compose collapses all those changes into one state snapshot. The derivedStateOf block is executed only once in the next recomposition pass, regardless of how many intermediate values were produced.
2. Dependency scope: The set of state reads that derivedStateOf tracks is captured for each individual recomposition. Concurrent recompositions are independent; each one sees the current snapshot of the source state at the moment the block runs. Dependencies are not shared across parallel recomposition passes.
Jetpack Compose’s scheduler batches state changes that occur before the next frame. When an update happens, the framework marks the affected State as “dirty” and schedules a recomposition. If more changes to the same State happen before that recomposition runs, the scheduler keeps only the last value. Consequently, the derivedStateOf block sees a single value and is recomputed once per frame.
val count = remember { mutableStateOf(0) }val doubled = derivedStateOf { count.value * 2 }count five times each with delay(0) so all updates happen in the same frame.LaunchedEffect(doubled) block.delay(16) between some increments to force a frame boundary; you should see two recompositions instead of one.Does your workload sometimes have coroutines that suspend and resume after a frame? If so, the derived state may be recomputed more than once per logical “round” of updates. Knowing whether updates cross frame boundaries will help you decide if you need additional throttling or memoization.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 22 Jun 2020, 11:30 UTC
A useful clarification: with the default structural-equality policy, derivedStateOf can skip recalculation entirely if concurrent writes leave the source at the same value last observed by the derived block; multiple writes do not force one recalculation per write. The DerivedState cache and dirty flag are shared across readers, but each recomposition pass records its own dependency reads against the current snapshot.
Version assumption: behavior reflects current androidx.compose.runtime snapshot semantics; exact collapse rules are implementation-defined and may change between Compose releases. Verify against your target runtime and compiler version.
Practical check: log inside the calculation block, apply several updates from coroutines before the next flush, and compare calculation count to update count. Repeat with a custom SnapshotMutationPolicy that always reports change to confirm equality policy governs skipping.