Lesson 2
Snapshots isolate values
Distinguish global, read-only, mutable, and nested snapshots by what they can see and when changes become visible.
Status: A snapshot is an isolated view of snapshot-aware state, not a lock or a copied application graph. That model is Durable . The
GlobalSnapshot,MutableSnapshot, and nested-snapshot classes are Version-specific at the pinned revision.
Outcome
Given a state write inside a snapshot, you should be able to say who can see it now, who can see it after a child apply, and who can see it after the parent applies.
The current snapshot is local context
Snapshot.enter { ... } makes one snapshot current for the duration of a block. Reads of snapshot-aware state select records for that snapshot. On exit, the previous current snapshot is restored. The implementation stores the current snapshot in thread-local context, so another thread is not changed merely because this block entered a snapshot (Snapshot.kt).
The global snapshot is the root view used when no explicit snapshot is current. Its role is visible in the public Snapshot documentation and the GlobalSnapshot implementation (Snapshot.kt). A read-only snapshot gives you a stable view. A mutable snapshot adds a private write set and an apply() boundary.
flowchart TD
Global[Global snapshot] --> Parent[Mutable snapshot]
Parent --> Child[Nested mutable snapshot]
Child --> ChildView[Child sees child writes]
Child -->|apply| ParentView[Parent sees child writes]
Parent -->|apply| GlobalView[Global sees parent writes]Mutable snapshots delay visibility
Inside takeMutableSnapshot(), a write creates or updates a record for that snapshot. The snapshot sees the new value immediately. A read outside it continues to see the old global value until apply() succeeds. The MutableSnapshot KDoc makes the boundary explicit: changes are isolated until apply, and a nested mutable snapshot applies to its parent rather than directly to global state (Snapshot.kt).
A nested snapshot is not merely a second function call. It has its parent’s view plus its own changes. Applying the child makes those changes visible to the parent. Applying the parent then makes the combined result visible globally. Every snapshot must eventually be applied or disposed; an active snapshot pins records and can retain memory (Snapshot.kt).
A read-only nested snapshot can outlive its parent after apply and disposal; an upstream regression test checks that it still sees the parent’s value. That is a lifetime rule for the view, not frozen global state.
Isolation is not serialization
Snapshot code uses synchronization to protect record and snapshot bookkeeping, but a snapshot does not make arbitrary mutable objects safe. If a state object contains a mutable list, the list itself is not magically versioned unless the list is a snapshot-aware state object. Use the snapshot mechanism to reason about reads and writes of StateObject values, not every object reachable from them.
Likewise, isolation does not mean that every apply can succeed. Two mutable snapshots can write the same state object, and the later apply may report a conflict. Lesson 4 explains policies and merges; for now, keep visibility and conflict resolution as separate questions.
Reproduce the visibility boundary
This is a pinned upstream-test procedure, not an experiment run for this lesson. In an AndroidX checkout at cc1caf65677fc10a8ce8116eba46e716f4cef222, run:
./gradlew :compose:runtime:runtime:desktopTest \
--tests 'androidx.compose.runtime.snapshots.SnapshotTests.snapshotsAreIsolatedFromGlobalChanges' \
--tests 'androidx.compose.runtime.snapshots.SnapshotTests.multipleSnapshotsAreIsolatedAndCanBeApplied' \
--tests 'androidx.compose.runtime.snapshots.SnapshotTests.aNestedMutableSnapshotCanBeAppliedToItsParent' \
--tests 'androidx.compose.runtime.snapshots.SnapshotTests.nestedSnapshotsAreIsolated'
The control is the read-only snapshotsAreIsolatedFromGlobalChanges case: a global write does not change the captured snapshot. The mutable cases should show each snapshot seeing its own writes, global state retaining its old value before apply, and all applied values visible after the final parent/root apply. Mutation: add a child snapshot and apply it before the parent. Expected: the parent sees the child’s value, while global does not until the parent applies. Interpretation limit: these tests establish snapshot visibility for the tested state objects. They do not establish recomposition timing, frame scheduling, or safety for ordinary mutable fields.
Misconceptions
- “Global means every thread sees a write immediately.” A write is local to the active snapshot until apply.
- “Nested means independent of the parent.” A nested mutable snapshot applies into its parent.
- “A snapshot is a lock.” It isolates snapshot-aware records; it does not protect arbitrary object graphs.
- “Disposal is optional cleanup.” An undisposed snapshot can keep records pinned.
Check yourself
A child writes B, applies to a parent that still sees A, and the parent has not applied. What should a global read return? What should a read inside the parent return?
Source notes
| Claim | Direct evidence | Status |
|---|---|---|
enter changes the current snapshot for the block and restores it | Snapshot.kt | Durable
Version-specific |
| Mutable snapshots isolate writes until apply; nested apply targets the parent | Snapshot.kt | Durable
Version-specific |
| Upstream tests cover global, sibling, nested, and disposal boundaries | SnapshotTests.kt | Version-specific |
Freshness
Refresh this lesson when current-snapshot context, global-snapshot advancement, nested apply, or disposal/pinning changes. The release notes page is also a freshness trigger because recent Compose Runtime releases have changed snapshot apply-observer behavior; do not infer current scheduling from this lesson’s isolation model.
Finished this lesson?
Your progress stays only in this browser.