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.

4 min readUpdated Jul 12, 2026

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.

Diagram
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]
  1. The write invalidates the observed scope; recomposition re-enters it. This is calculation, not a LayoutNode mutation.
  2. If the updater value differs, an UpdateNode operation is recorded and applyChanges invokes it through UiApplier.
  3. The setter selects measurement, placement, or draw invalidation. Requests can be coalesced (NodeKind.kt#L338, MeasureAndLayoutDelegate.kt#L200).
  4. A later measure/layout pass consumes pending work; NodeCoordinator.draw is 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

ClaimDirect evidenceStatus
A node update can schedule targeted measure/layout/draw invalidationNodeKind.kt#L338Durable Version-specific
Independent probes cover composition/apply, queues, drawing, and hit pathsCompositionTests.kt#L3917-L3970, MeasureAndLayoutDelegateTest.kt#L1046-L1070, DrawModifierTest.kt#L126-L143, PointerInputEventProcessorTest.kt#L312-L362Version-specific
Measure/layout work is consumed separately from composition applicationComposition.kt and MeasureAndLayoutDelegate.ktDurable Version-specific
Pointer down builds a hit path and dispatches through itPointerInputEventProcessor.kt and HitPathTracker.ktVersion-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.