Lesson 2
Historical concurrent recomposition and current pausable composition
Verify the removed concurrent runner, then inspect current pausable composition and its serialized application boundary.
Status: “Calculate a change list, then apply it” is Durable . The public concurrent runner is removed at the pinned revision; current
PausableComposition,PausedCompositionImpl,ShouldPauseCallback, andRecordingApplierdetails are Version-specific . The Experimental label below marks the removed, changing history—not a current Kotlin opt-in annotation.
Outcome
Explain what may run away from the applier, where mutable-snapshot conflicts are detected, and why a paused composition is not usable until apply().
Concurrency has a hard boundary
The current Recomposer has runRecomposeAndApplyChanges() but no runRecomposeConcurrentlyAndApplyChanges in its public API or current source (runtime API signature, Recomposer.kt). The file still contains private concurrentCompositionsOutstanding and ProduceFrameSignal remnants, but no current concurrent runner call site; those symbols do not make concurrent recomposition a supported mode.
The pinned Recomposer wraps composition in a mutable snapshot. applyAndCheck reports an unsupported concurrent modification on failure (Recomposer.kt). Snapshot application compares previous, current, and applying records; a policy may merge, otherwise check() throws SnapshotApplyConflictException (Snapshot.kt). Conflict handling therefore belongs to snapshot/application boundaries, not to the paused-composition completion flag.
Historically, the experimental runRecomposeConcurrentlyAndApplyChanges path launched invalidated compositions on a recompose context and queued results; one frame loop drained the queue and called applyChanges (historical Recomposer.kt). Calculation could be parallel; applier operations stayed serialized. AndroidX removed that API in 1.11.0-alpha01 and cited pausable composition as its successor (removal commit). This historical path is not a current programming model.
flowchart LR
Compose[Compose work] --> Snapshot[Mutable snapshot]
Snapshot --> Changes[Recorded changes]
Changes --> Queue[Apply boundary]
Queue --> Applier[Serialized applier]
Snapshot -->|Conflict| Failure[Apply failure]Pausing records progress, not just suspension
The current PausableComposition and PausedComposition APIs have no @ExperimentalComposeApi or @ExperimentalComposeRuntimeApi marker in the pinned runtime API signature. The API entered the runtime 1.8.0-alpha02 line and is present in the stable androidx.compose.runtime:runtime:1.10.0 line; compiler support and private state details still need a version check (1.8.0-alpha02 release notes, runtime API signature).
PausableComposition creates a PausedComposition that starts incomplete. resume(ShouldPauseCallback) advances until a pause point or completion; the callback is only a request. PausedCompositionImpl tracks pending, recomposing, apply-pending, applied, failure, and cancellation states (PausableComposition.kt).
During resume, RecordingApplier records node and property operations instead of mutating the real applier. Only apply() locks the composition and plays them into the real applier, then dispatches queued callbacks (PausableComposition.kt). A complete calculation is not visible content. isComplete has a narrower meaning: the pinned API defines it as the last resume completion result and the precondition for apply; it can become false when state read while paused changes (PausableComposition.kt, apply precondition). It does not prove the recorded result remains uninvalidated or conflict-free. Check it immediately before apply(), and let the snapshot/application boundary handle invalidation or conflicts.
Reproduce both boundaries
At the current pin, run:
git checkout cc1caf65677fc10a8ce8116eba46e716f4cef222
./gradlew :compose:runtime:runtime:desktopTest \
--tests 'androidx.compose.runtime.PausableCompositionTests.applierOnlyCalledInApply' \
--tests 'androidx.compose.runtime.PausableCompositionTests.rememberOnlyCalledInApply' \
--tests 'androidx.compose.runtime.PausableCompositionTests.resumeOnBackgroundThread'
Controls: the first test checks no real-applier call during set/resume; the second checks callback timing; the third resumes on another thread. Expected: work is inert until apply, callbacks follow application, and resume completes. Not executed in this content edit. Mock appliers do not prove arbitrary target-tree thread safety.
To inspect the removed concurrent behavior instead, checkout 5d58092cb9972d25055f07109056364acee983c2 and run:
./gradlew :compose:runtime:runtime:jvmTest \
--tests 'androidx.compose.runtime.RecomposerTestsJvm.concurrentRecompositionOffMainThread'
Expected: recomposition uses a different thread while application remains in the runner. Not executed in this content edit. Limit: historical only; not a current contract.
Misconceptions and check
- “Concurrent recomposition mutates the target tree concurrently.” The historical design serialized apply; pausable composition records first.
- “Paused means coroutine suspension.” It is a composition state machine with recorded applier operations.
- “A successful composition is already visible.” Visibility waits for application.
Two snapshots both start at 0; one writes 1, the other writes 2, and the first applies. Which side of the composition/application boundary sees the conflict, and why does the second write not become visible?
Evidence ledger
| Claim | Direct evidence | Status |
|---|---|---|
| Composition uses a mutable snapshot and rejects an unresolved apply conflict | Recomposer.kt, Snapshot.kt | Durable
Version-specific |
| Current public API has no concurrent runner; historical calculation fed one serialized apply loop | runtime API signature, historical Recomposer.kt, removal commit | Version-specific
Experimental |
| Current pausable API has no experimental opt-in marker; resume and apply remain distinct states | runtime API signature, PausableComposition.kt | Version-specific |
| RecordingApplier defers real applier calls | PausableComposition.kt, PausableCompositionTests.kt | Version-specific |
Freshness
Refresh after changes to composing, snapshot merge/conflict handling, PausableComposition, compiler pause support, or the release-note status of concurrent recomposition. Keep Chapter 8’s effect semantics out of this lesson; only the application boundary is relevant here.
Finished this lesson?
Your progress stays only in this browser.