Lesson 1

The recomposition loop and frame clock

Trace snapshot invalidation through a frame, then separate recomposition from change application.

3 min readUpdated Jul 12, 2026

Status: The separation between scheduling, composition, and application is Durable . The state names, queues, and frame ordering below are Version-specific at the pinned AndroidX revision.

Outcome

Given a state write, explain when it becomes recomposition work, when a frame is requested, and why recompose, applyChanges, and applyLateChanges are different calls.

A recomposer is a work-state machine

Recomposer.State distinguishes inactive, idle, pending-work, and shutdown states. Its lock-protected lists hold snapshot invalidations, invalidated compositions, and work awaiting application (Recomposer.kt). A state assignment first becomes a notification, then scheduled work.

The snapshot observer filters known unread state into snapshotInvalidations. recordComposerModifications() sends it to known compositions, which resolve values to restart scopes (Recomposer.kt). The body has not run yet.

Diagram
flowchart LR
    Write[State write] --> Notify[Snapshot notify]
    Notify --> Queue[Invalidation queue]
    Queue --> Frame[Frame opportunity]
    Frame --> Compose[Recompose]
    Compose --> Apply[Apply changes]
    Apply --> Late[Late changes]

A frame clock is a delivery mechanism

The loop waits for work, then aligns it with parentFrameClock.withFrameNanos. Before recomposing, it sends the timestamp to current withFrameNanos callers through BroadcastFrameClock, then sends snapshot notifications so those writes can join the frame (Recomposer.kt, BroadcastFrameClock.kt). The broadcast clock resumes awaiters; it does not compose.

ProduceFrameSignal remains in the pinned file as a conflated signal, but has no ordinary-loop call site. The removed concurrent runner used it for background work; AndroidX later named pausable composition as its successor (removal change, Recomposer.kt archaeology). Treat it as history.

Calculation comes before mutation

ControlledComposition.recompose() calculates pending changes and says no tree changes have happened. applyChanges() executes main operations, applyLateChanges() performs inserts that must wait for removals or moves, and changesApplied() closes the cycle (Composition.kt). The pinned loop uses that order, then calls Snapshot.notifyObjectsInitialized() (Recomposer.kt). As a boundary marker only, recorded lifecycle callbacks and side effects run after applier operations during apply; Chapter 8 covers their APIs (Composition.kt).

Reproduce the ordering

From an AndroidX checkout at the pin:

git checkout cc1caf65677fc10a8ce8116eba46e716f4cef222
./gradlew :compose:runtime:runtime:desktopTest \
  --tests 'androidx.compose.runtime.RecomposerTests.recomposerRecomposesWhileOpen' \
  --tests 'androidx.compose.runtime.RecomposerTests.stateChangesDuringApplyChangesAreNotifiedBeforeFrameFinished' \
  --tests 'androidx.compose.runtime.RecomposerFrameEndSchedulingTests.scheduleFrameEnd_Callback_invokesAfterMovableContentSettles'

Controls: state mutation, an apply-time write, and a frame-end callback. Expected: mutation is not synchronous; apply-time writes notify before the frame ends; the callback waits for late work. Not executed in this content edit. It covers the pinned common runtime, not every dispatcher.

Misconceptions and check

  • “Every frame recomposes every composition.” Only queued work is considered.
  • “The frame clock performs composition.” It delivers time and wakes work; Recomposer owns composition.
  • “Apply notifications mean the tree changed.” Notifications hand state to observers; application is later.

A composition read state, then it changed. Which two boundaries must occur before it reflects it: notification-to-invalidation, or recomposition-to-application? Why do both matter?

Evidence ledger

ClaimDirect evidenceStatus
Recomposer state and invalidation handoff are separate stagesRecomposer.kt, Recomposer.ktDurable Version-specific
Broadcast delivery is distinct from compositionBroadcastFrameClock.ktDurable Version-specific
Main, late, and completion phases are orderedRecomposer.kt, Composition.ktVersion-specific
ProduceFrameSignal is historical hereremoval change, Recomposer.ktExperimental

Freshness

Refresh after changes to runRecomposeAndApplyChanges, snapshot routing, BroadcastFrameClock, ProduceFrameSignal, or the apply contract. Recheck the Compose Runtime release notes before mapping this ordering to a release.

Finished this lesson?

Your progress stays only in this browser.