Lesson 1
A state write targets observed scopes
Trace snapshot notification into Recomposer, CompositionImpl, and a RecomposeScopeImpl without confusing invalidation with execution.
Status: A read creates an observation edge and a later write can target the scopes on that edge. That causal model is Durable . The queue names and handoff methods are Version-specific at the pinned AndroidX revision.
Outcome
Name the recipient of a state change at each boundary: snapshot notification, known composition, observed restart scope, and recomposer work list. Say what has not happened yet.
The write does not invalidate the whole tree
During composition, CompositionImpl.recordReadOf associates the current RecomposeScopeImpl with the value it read. It also records derived-state dependencies when the value is a DerivedState (Composition.kt). The runtime therefore has a reverse lookup: value → scopes that observed it.
A snapshot apply notification gives Recomposer changed state objects. Its pinned observer filters out state known never to have been read in composition, then adds the rest to snapshotInvalidations (Recomposer.kt). This is a notification set, not a list of functions to call.
flowchart LR
Read[Scope reads state] --> Edge[Observation edge]
Write[Snapshot write] --> Notify[Snapshot notification]
Notify --> Recomposer[Recomposer handoff]
Recomposer --> Composition[CompositionImpl]
Composition --> Scope[Observed restart scope]
Scope --> Work[Pending recomposition work]The handoff has two distinct steps
Recomposer.recordComposerModifications() takes pending snapshot values and sends them to each known ControlledComposition through recordModificationsOf (Recomposer.kt). CompositionImpl.recordModificationsOf queues them if changes are already waiting, then drains them into its observation map (Composition.kt).
For each scope that observed the value, addPendingInvalidationsLocked calls scope.invalidateForResult(value). RecomposeScopeImpl delegates that call to its owner; its anchor and restart callback are the scope’s durable connection to composition memory (RecomposeScopeImpl.kt). CompositionImpl.invalidate rejects a removed, not-yet-owned, or non-recomposable scope. Otherwise it records the scope’s invalidation and tells its parent to invalidate the composition (Composition.kt).
In the ordinary managed path, the parent is Recomposer. It adds the composition to compositionInvalidations once (Recomposer.kt). This chapter stops at that handoff; Chapter 7 explains scheduling and change-application ordering.
What “invalidated” means here
Invalidation means “this scope must be considered when recomposition is requested.” It does not mean the body has already run, a node has changed, or a frame has completed. ControlledComposition.recompose() explicitly calculates pending changes while applyChanges() performs them (Composition.kt).
Reproduce the boundary
From a clean AndroidX checkout at cc1caf65677fc10a8ce8116eba46e716f4cef222, inspect the pinned test and run:
./gradlew :compose:runtime:runtime:desktopTest \
--tests 'androidx.compose.runtime.CompositionTests.testSimpleSkipping' \
--tests 'androidx.compose.runtime.CompositionTests.testInvalidationAfterRemoval'
The control is testSimpleSkipping: it invalidates a live scope with equal inputs and expects no changes. The removal case invalidates live content, removes the branch, then invalidates the old scope and expects no changes. This procedure was not executed in this content edit; it does not establish frame timing or another composer’s behavior.
Check yourself
A state object was read by two restart scopes, but only one scope’s group was removed. Which object should the runtime discard: the state, the composition, or the removed scope’s observation edge? Why?
Evidence ledger
| Claim | Direct evidence | Status |
|---|---|---|
| Reads map values to current restart scopes | Pinned Composition.kt, recordReadOf | Durable
Version-specific |
| Recomposer propagates snapshot values to known compositions | Pinned Recomposer.kt | Version-specific |
| Composition invalidation validates scope ownership and notifies its parent | Pinned Composition.kt | Version-specific |
| Removed scopes do not produce later changes | Pinned CompositionTests.kt, testInvalidationAfterRemoval | Version-specific |
Freshness
Recheck this lesson when snapshot apply notifications stop filtering unread state, recordModificationsOf changes its queueing contract, or CompositionImpl.invalidate changes its ownership checks. Do not expand it into frame scheduling; that boundary is reserved for Chapter 7.
Finished this lesson?
Your progress stays only in this browser.