Lesson 5
Reads become observation edges
Map snapshot reads to composition scopes and SnapshotStateObserver callbacks without jumping ahead to frame scheduling.
Status: Turning a read into an observation edge is Durable . The composition maps, reader flags, apply queue, and executor behavior are Version-specific at the pinned revision.
Outcome
After a state read, you should be able to identify the recorded edge and distinguish it from the later work that a scheduler may perform.
A read has an owner
SnapshotMutableState documents the central boundary: reading value during a composable subscribes the current RecomposeScope to that value, while writes are transacted through snapshots (SnapshotMutableState.kt). The read reports the state object to the active snapshot observer; composition decides which scope is current.
At the pinned revision, CompositionImpl.recordReadOf marks the current scope as used, records the value in the scope’s tracked instances, and adds a value-to-scope entry in observations. If the value is a StateObjectImpl, it also records that the composition reader saw it (Composition.kt). A later recordWriteOf looks up scopes for the written value and also follows derived-state dependency mappings (Composition.kt).
flowchart LR
Read[State read] --> Scope[Current composition scope]
Scope --> Edge[Value to scope edge]
Write[State write] --> Change[Changed state set]
Change --> Edge
Edge --> Later[Later runtime work]The last box is intentionally vague. The edge records who observed whom. It does not claim that a body has already run again, that a frame has been requested, or that a node has changed. Those are Chapter 6 and Chapter 7 boundaries.
SnapshotStateObserver is a reusable read map
SnapshotStateObserver offers a similar mechanism for non-composition scopes such as layout or draw helpers. Call start() to register its snapshot apply observer. Call observeReads(scope, onChanged, block) to execute a block while recording the state objects it reads. The observer stores a value-to-scope map and a reverse scope-to-values map so it can remove stale reads and find affected scopes (SnapshotStateObserver.kt).
When an apply observer reports changed objects, SnapshotStateObserver queues the set, records invalidation in each scope map, and uses onChangedExecutor to run callbacks. The callback receives the scope value, not the changed state object. It does not rerun the observed block; the owner decides what to do next. The class is not thread-safe across observation threads.
A read can be intentionally excluded with Snapshot.withoutReadObservation { ... }. This matters when bookkeeping reads should not subscribe a scope. The pinned tests cover paused observation and nested observation, so do not reduce the mechanism to “every property access registers forever.”
Snapshot apply is the handoff
The observer does not treat a setter call as a complete notification. The snapshot system collects modifications and sends apply notifications at its own boundary. In the upstream test, the sequence is explicit: start the observer, observe a state read, call Snapshot.notifyObjectsInitialized(), mutate the state, then call Snapshot.sendApplyNotifications(). The callback count becomes one (SnapshotStateObserverTestsCommon.kt).
The sequence separates three events: a scope reads a state object, a snapshot records a changed object, and an observer callback is dispatched. Composition uses its own read/write hooks; do not replace its map with SnapshotStateObserver.
Reproduce both observation paths
This is a pinned upstream-test procedure, not an experiment run for this lesson. At AndroidX revision cc1caf65677fc10a8ce8116eba46e716f4cef222, run:
./gradlew :compose:runtime:runtime:desktopTest \
--tests 'androidx.compose.runtime.snapshots.SnapshotStateObserverTestsCommon.stateChangeTriggersCallback' \
--tests 'androidx.compose.runtime.snapshots.SnapshotStateObserverTestsCommon.withoutReadObservationStopsObserving' \
--tests 'androidx.compose.runtime.snapshots.SnapshotStateObserverTestsCommon.multipleStagesWorksTogether' \
--tests 'androidx.compose.runtime.CompositionAndDerivedStateTests.observingBothNormalAndDerivedInSameScope'
The control is stateChangeTriggersCallback: one observed read followed by one apply-notification cycle should call the scope callback once. The mutation is withoutReadObservationStopsObserving: perform the read inside Snapshot.withoutReadObservation; that read should not create the observation edge. The composition test controls a normal state read and a derived read in one scope, showing that the composition maintains both forms of dependency bookkeeping. Interpretation limit: these tests show registration and callback boundaries. They do not prove callback latency, recomposer scheduling, frame behavior, or a particular UI invalidation policy.
Misconceptions
- “A state write reruns the composable immediately.” The write and observation edge precede later invalidation and scheduling.
- “
SnapshotStateObserveris the composition.” It is a separate reusable observer with scope callbacks. - “Every read is permanent.” Observation is refreshed, stale reads are removed, and reads can be suppressed.
- “Apply notification is a frame.” It is a snapshot handoff, not a frame-clock event.
Check yourself
What should change when a state is read inside withoutReadObservation? Name the map or edge that should remain absent, and name the later event that this does not predict.
Source notes
| Claim | Direct evidence | Status |
|---|---|---|
| Composition records state reads and writes against scopes | Composition.kt | Durable
Version-specific |
SnapshotStateObserver records reads and dispatches changed scopes | SnapshotStateObserver.kt | Version-specific |
| Snapshot apply notifications drive observer callbacks | SnapshotStateObserverTestsCommon.kt | Version-specific |
| Derived and normal reads can coexist in composition bookkeeping | CompositionAndDerivedStateTests.kt | Version-specific |
Freshness
Refresh this lesson when recordReadOf, recordWriteOf, SnapshotStateObserver, apply-notification delivery, or read-suppression APIs change. The release notes currently mention fixes to derived-state tracking and apply observers; those changes are reasons to recheck observation tests before making a scheduling claim.
Finished this lesson?
Your progress stays only in this browser.