Lesson 4
One state change reaches pixels and hit paths
Trace one observed state write through recomposition, node application, measurement, placement, drawing, and pointer hit-path dispatch.
Status: The separation of recomposition, remeasurement, relayout, and redraw is Durable . The exact UI and pointer traversal below is Version-specific .
Outcome
Trace one observed state write without saying “Compose redraws.” Name the boundary at each step and identify which steps may be skipped.
Five different events
Keep the names separate: recomposition runs invalidated composition code; apply mutates the target tree; remeasurement recalculates sizes; relayout places already-measured nodes; and redraw refreshes pixels or layers. They form a chain only when an update requires the next step. A recomposition can calculate no node change, and a draw-only update need not remeasure.
The trace is a synthesis, not one test
Assume Title(text = state.value) has read state in a composition scope, and its text property is represented by an updater block. The diagram below is a teaching synthesis of independently tested boundaries. No upstream test emits this unified trace.
flowchart TD
Write[Observed state write] --> Recompose[Recomposition calculates]
Recompose --> Apply[ChangeList applies node update]
Apply --> Requests[Targeted UI requests]
Requests --> Measure[Optional remeasure]
Requests --> Layout[Optional relayout]
Measure --> Layout
Layout --> Draw[Optional redraw]
Draw --> Hit[Later pointer hit path]- The write invalidates the observed scope; recomposition re-enters it. This is calculation, not a
LayoutNodemutation. - If the updater value differs, an
UpdateNodeoperation is recorded andapplyChangesinvokes it throughUiApplier. - The setter selects measurement, placement, or draw invalidation. Requests can be coalesced (
NodeKind.kt#L338,MeasureAndLayoutDelegate.kt#L200). - A later measure/layout pass consumes pending work;
NodeCoordinator.drawis a separate traversal (MeasureAndLayoutDelegate.kt#L401,NodeCoordinator.kt#L496).
Where pointer input joins
Pointer processing does not mean recomposition. On a down event, PointerInputEventProcessor.process hit-tests the current layout tree. If it finds pointer-input modifier nodes, it adds their ancestor-to-descendant path to HitPathTracker; subsequent changes dispatch through that cached hierarchy (HitPathTracker.kt). It uses current LayoutNode coordinates and modifier nodes; it does not imply whole-tree recomposition or a draw pass.
Independently reproducible probes
Run these exact probes at the pinned checkout. They prove named boundaries separately; none emits the synthesis above as one trace.
Composition and apply:
./gradlew :compose:runtime:runtime:desktopTest \
--tests 'androidx.compose.runtime.CompositionTests.composeNodeSetVsUpdate'
CompositionTests.composeNodeSetVsUpdate composes two nodes, changes state from initial to changed, and asserts both properties change; set has counts 1 → 2 and update has 0 → 1 (CompositionTests.kt#L3917-L3970). This proves the runtime fixture’s state-to-updater boundary, not Android UiApplier frame timing.
Targeted measure and layout:
./gradlew :compose:ui:ui:connectedAndroidTest \
-Pandroidx.testInstrumentationRunnerArguments.class=androidx.compose.ui.layout.MeasureAndLayoutDelegateTest#requestRemeasureTriggersModifierRemeasure
./gradlew :compose:ui:ui:connectedAndroidTest \
-Pandroidx.testInstrumentationRunnerArguments.class=androidx.compose.ui.layout.MeasureAndLayoutDelegateTest#requestRelayoutTriggersModifierRelayout
The first asserts a SpyLayoutModifier measure count increases by one; the second asserts no extra measure and exactly one layout. Both call measureAndLayout() and assert it returns false (MeasureAndLayoutDelegateTest.kt#L1046-L1070).
Draw and pointer boundaries:
./gradlew :compose:ui:ui:connectedAndroidTest \
-Pandroidx.testInstrumentationRunnerArguments.class=androidx.compose.ui.DrawModifierTest#recomposeDrawTest
./gradlew :compose:ui:ui:connectedAndroidTest \
-Pandroidx.testInstrumentationRunnerArguments.class=androidx.compose.ui.input.pointer.PointerInputEventProcessorTest#process_downHits_targetReceives
./gradlew :compose:ui:ui:connectedAndroidTest \
-Pandroidx.testInstrumentationRunnerArguments.class=androidx.compose.ui.input.pointer.HitPathTrackerTest#addHitPath_completeMatchingTree_resultIsCorrect
recomposeDrawTest changes model colors and validates the new pixels; process_downHits_targetReceives asserts four initial-pass pointer deliveries with coordinates translated into the child; addHitPath_completeMatchingTree_resultIsCorrect asserts the shared three-node path and both pointer IDs (DrawModifierTest.kt#L126-L143, PointerInputEventProcessorTest.kt#L312-L362, HitPathTrackerTest.kt#L227-L258). These upstream procedures were not executed in this content edit; they do not establish host frame ordering or emit one unified log.
Misconceptions
- “Recomposition is remeasurement.” Recomposition calculates composition work; measurement consumes a UI invalidation queue later.
- “Relayout always remeasures.” Placement can be invalidated independently when size is still valid.
- “Redraw means the node recomposed.” A draw/layer invalidation can be downstream work with no new composition execution.
- “Hit testing is part of recomposition.” Pointer processing walks current coordinates and modifier nodes during event dispatch.
Check yourself
If a state change updates only a draw modifier’s color, which of the five events must happen, which may be skipped, and when does pointer hit-path tracking run?
Evidence ledger
| Claim | Direct evidence | Status |
|---|---|---|
| A node update can schedule targeted measure/layout/draw invalidation | NodeKind.kt#L338 | Durable
Version-specific |
| Independent probes cover composition/apply, queues, drawing, and hit paths | CompositionTests.kt#L3917-L3970, MeasureAndLayoutDelegateTest.kt#L1046-L1070, DrawModifierTest.kt#L126-L143, PointerInputEventProcessorTest.kt#L312-L362 | Version-specific |
| Measure/layout work is consumed separately from composition application | Composition.kt and MeasureAndLayoutDelegate.kt | Durable
Version-specific |
| Pointer down builds a hit path and dispatches through it | PointerInputEventProcessor.kt and HitPathTracker.kt | Version-specific |
Freshness
Refresh when invalidation masks, owner scheduling, coordinator drawing, or pointer hit-path caching changes. Keep this as one causal trace; detailed layout policies and pointer gesture APIs belong elsewhere.
Finished this lesson?
Your progress stays only in this browser.