Lesson 6
Follow one state change through the pipeline
Use a controlled composition to distinguish invalidation, recalculation, and applied node updates.
Status: The separation between observed state, recomposition calculation, and application is Durable . The controlled API sequence below is Version-specific at the pinned revision.
Outcome
You should be able to predict which event changes during a state update:
- the state object is written;
- an observed scope is made eligible for recomposition;
- composition calculates pending operations; or
- the applier mutates the target tree.
These are related events. They are not one event called “recomposition.”
Start with a no-node control
Use two composables:
@Composable
fun Root(label: MutableState<String>) {
Marker()
Emitted(label.value)
}
@Composable
fun Marker() { /* reads no node-producing API */ }
Marker is a no-node control. This exact example demonstrates group participation and no node emission; because it reads no state, it does not demonstrate state observation. Emitted is the node-producing branch. The experiment needs a logging applier and a mutable state value.
Run the controlled sequence
The pinned ControlledComposition API makes the phases visible:
composition.composeContent { Root(state) }
composition.applyChanges()
println(composition.recompose()) // no write: false
state.value = "second" // write observed by Root
composition.recordModificationsOf(setOf(state))
println(composition.recompose()) // pending work: true
composition.applyChanges() // send changes to the applier
The explicit recordModificationsOf call matters. This controlled handoff supplies the invalidation set. A changed function argument by itself is not evidence that a controlled composition has discovered invalidation.
What to observe
A logging LogApplier should show a shape like this:
| Moment | Expected observation |
|---|---|
Initial applyChanges | One node insertion and the first property application |
No-write recompose | No pending work |
| State write plus handoff | The observed scope becomes pending |
Invalidated recompose | Work is calculated, but not yet applied |
Second applyChanges | One property update and no second insertion |
Exact test-harness output can include framework lines. Assert operation counts and root.children, not log formatting or timing.
The causal map
flowchart TD
Write[State write] --> Invalidate[Observed scope invalidated]
Invalidate --> Recompose[Recompose calculates]
Recompose --> Pending[Pending change]
Pending --> Apply[applyChanges]
Apply --> Update[Applier updates node]The diagram does not claim that every state write immediately runs a frame. Scheduling belongs to a later chapter. Here we are isolating the controlled composition boundary.
What this proves, and what it does not
This experiment supports a bounded claim: an observed state write can invalidate a scope; recompose() can calculate pending work; applyChanges() can perform the applier operation.
It does not prove a universal scheduling policy, a fixed compiler output shape, or a performance result. It also does not show that every recomposition inserts nodes. The no-node control exists to make that last boundary visible.
Check yourself
If the second run changes only a node property, where should the observable mutation appear? Which event should you not infer from that observation?
Source notes
| Claim | Direct evidence | Status |
|---|---|---|
Controlled composition separates recompose and applyChanges | Composition.kt, ControlledComposition | Durable
Version-specific |
| Reads and writes connect composition scopes to invalidation | Composition.kt, recordReadOf and recordWriteOf | Version-specific |
| Runtime tests cover bounded restart and skip behavior | RestartTests.kt | Version-specific |
Freshness
Refresh this lesson when controlled invalidation, recompose, or applyChanges changes. Rerun the logging fixture at the new pinned revision; do not turn its output formatting into a contract.
Finished this lesson?
Your progress stays only in this browser.