Lesson 3
A node update fans out into UI work
Trace modifier-chain updates into targeted requests for remeasurement, relayout, and redraw.
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 kind | Possible request | Not automatically implied |
|---|---|---|
| Measure policy or layout modifier | Remeasure | Redraw of every descendant |
| Placement-sensitive node | Relayout | Recomposition |
| Draw modifier | Redraw/layer invalidation | Remeasure |
| A modifier-chain structural update | Several targeted invalidations | A 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.”
requestRemeasureandrequestRelayoutenqueue work for a latermeasureAndLayoutexecution.
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
| Claim | Direct evidence | Status |
|---|---|---|
ModifierNodeElement creates and updates modifier nodes | ModifierNodeElement.kt and NodeChain.kt#L153-L170 | Durable
Version-specific |
| Pinned diff probe observes same-kind reuse and structural edits | NodeChainTests.kt#L160-L188 | Version-specific |
| Pinned queue probes distinguish remeasure from relayout | MeasureAndLayoutDelegateTest.kt#L1046-L1070 | Version-specific |
| Node kinds map updates to measure, layout, and draw invalidation | NodeKind.kt | Version-specific |
MeasureAndLayoutDelegate queues and consumes measure/layout work | MeasureAndLayoutDelegate.kt | Durable
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.