Lesson 3

A node update fans out into UI work

Trace modifier-chain updates into targeted requests for remeasurement, relayout, and redraw.

4 min readUpdated Jul 12, 2026

Status: Recomposition, remeasurement, relayout, and redraw are distinct Durable stages. The node-kind invalidation map and queue implementation are Version-specific .

Outcome

Given a changed ComposeUiNode property or modifier element, predict which downstream phase can be requested—and which phases are not implied.

A modifier is a node chain, not only a value

LayoutNode.modifier is a property update surface. Once attached, LayoutNode.applyModifier stores the new modifier, calls nodes.updateFrom(modifier), and updates parent data (LayoutNode.kt). NodeChain.updateFrom compares the old and new element vectors. When elements are the same kind but changed, it calls the element’s update(node); structural differences use a diff that inserts and removes modifier nodes (NodeChain.kt). A ModifierNodeElement supplies create() and update(node) (ModifierNodeElement.kt).

Invalidation is targeted

At the pinned NodeKind map, an updated layout modifier invalidates measurement; an OnRemeasured node invalidates measurements; an OnPlaced node requests relayout; and a draw node calls invalidateDraw() (NodeKind.kt). The map is a useful causal boundary:

Change or node kindPossible requestNot automatically implied
Measure policy or layout modifierRemeasureRedraw of every descendant
Placement-sensitive nodeRelayoutRecomposition
Draw modifierRedraw/layer invalidationRemeasure
A modifier-chain structural updateSeveral targeted invalidationsA full-tree rebuild

LayoutNode.measurePolicy calls invalidateMeasurements() when its value changes (LayoutNode.kt). MeasureAndLayoutDelegate marks pending work and queues it when applicable (MeasureAndLayoutDelegate.kt). These are requests, not the work itself.

The next pass consumes the queue

MeasureAndLayoutDelegate.measureAndLayout() drains scheduled nodes. For a pending measurement it runs the measure path; for pending placement it runs the layout path and dispatches positioned callbacks (MeasureAndLayoutDelegate.kt). Measurement can change size and trigger affected parents; relayout can change position without recomputing every size. A successful recomposition does not itself prove that either queue is non-empty.

Drawing is later again. NodeCoordinator.draw either draws a layer or translates to the node position and traverses contained draw modifiers; LayoutNodeDrawScope invokes the selected DrawModifierNode (NodeCoordinator.kt, LayoutNodeDrawScope.kt). A draw invalidation asks for fresh drawing; it is not a second recomposition.

Reproduce the boundaries with exact probes

These are separate upstream probes, not one test that emits a complete modifier-to-frame trace. At the pinned checkout, run each method on a connected Android device:

./gradlew :compose:ui:ui:connectedAndroidTest \
  -Pandroidx.testInstrumentationRunnerArguments.class=androidx.compose.ui.node.NodeChainTests#testSimple
./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

Probe 1 — diff observable: NodeChainTests.testSimple starts with a1, b1, c1, then applies a2, c2. Its assertions are [a,c], the diff a / -b / c, then entA === entA2 after inserting modifierD() while entC !== entB2 (NodeChainTests.kt#L160-L188). This proves same-kind compatibility/reuse versus structural removal/insertion. Because its parameters are equal, it does not prove a changed-parameter ModifierNodeElement.update callback; the source-only ActionUpdate branch calls updateNode and logs nodeUpdated (NodeChain.kt#L153-L170).

Probe 2 — remeasure queue: requestRemeasureTriggersModifierRemeasure attaches a SpyLayoutModifier, requests remeasurement on root.first, and calls measureAndLayout(). The test asserts the spy measure count increases by exactly one and the delegate returns false (MeasureAndLayoutDelegateTest.kt#L1046-L1057).

Probe 3 — relayout queue: requestRelayoutTriggersModifierRelayout uses the same spy, asserts its measure count does not change, then asserts layout count increases by exactly one after requestRelayout(root.first) and measureAndLayout() returns false (MeasureAndLayoutDelegateTest.kt#L1059-L1070). Limits: these probes establish diff and queue boundaries, not a universal frame timeline. They were not executed in this production run.

Misconceptions

  • “Modifiers are immutable wrappers only.” Elements can create and update attached mutable modifier nodes.
  • “Every modifier update remeasures and redraws everything.” Node kinds select narrower invalidation paths.
  • “A request is a completed pass.” requestRemeasure and requestRelayout enqueue work for a later measureAndLayout execution.

Check yourself

A draw-only modifier changes its color but not its size or placement. Which phase must be invalidated, and which two phases should you not infer from that fact?

Evidence ledger

ClaimDirect evidenceStatus
ModifierNodeElement creates and updates modifier nodesModifierNodeElement.kt and NodeChain.kt#L153-L170Durable Version-specific
Pinned diff probe observes same-kind reuse and structural editsNodeChainTests.kt#L160-L188Version-specific
Pinned queue probes distinguish remeasure from relayoutMeasureAndLayoutDelegateTest.kt#L1046-L1070Version-specific
Node kinds map updates to measure, layout, and draw invalidationNodeKind.ktVersion-specific
MeasureAndLayoutDelegate queues and consumes measure/layout workMeasureAndLayoutDelegate.ktDurable Version-specific

Freshness

Refresh when NodeChain diff actions, node-kind masks, invalidation requests, measure/layout queue policy, or coordinator/layer drawing changes. Keep this phase map separate from recomposer scheduling and pointer dispatch.

Finished this lesson?

Your progress stays only in this browser.