Lesson 2

`ComposeNode` crosses into `LayoutNode`

Follow createNode, useNode, updater blocks, and the Android UiApplier into the concrete Compose UI tree.

4 min readUpdated Jul 12, 2026

Status: The create/use/update distinction is Durable . The current helper overloads, UI node properties, and Android construction path are Version-specific .

Outcome

Given a node-producing composable, read the source-level create/use branch and use a pinned composition fixture to distinguish initial node properties from later updater work.

ComposeNode is a small protocol wrapper

The pinned ComposeNode first checks that the current applier has the expected type, starts a node group, then chooses createNode(factory) while currentComposer.inserting is true or useNode() otherwise. It runs the Updater block and ends the node group. A later execution can therefore revisit the same composition position without constructing a second target object.

Updater decides what is worth applying

Updater.set(value, block) remembers the value and schedules the block when inserting or when the remembered value differs. Updater.update(value, block) also records the new value, but skips the block on insertion because the constructor callback is expected to have supplied the initial value. init runs only during insertion; reconcile always schedules its block (Composer.kt). These are not direct property assignments from the composable body: composer.apply records work for the apply phase.

The optional skippableUpdate overload adds a composable update group. SkippableUpdater.update wraps its block in a replaceable group, so ordinary invalidation and skipping can avoid the expensive calculation inside that block. It does not make the node immune to updates, nor does it make every Updater.update skippable.

The concrete Android path

ComposeUiNode is the UI-side property surface: measure policy, layout direction, density, modifier, view configuration, composition locals, and composite key hash (ComposeUiNode.kt). Its Constructor points to LayoutNode.Constructor, and its preallocated setter lambdas turn updater work into property writes.

At the current Android pin, UiApplier is an AbstractApplier<LayoutNode>. It intentionally ignores insertTopDown, inserts in insertBottomUp with current.insertAt, delegates removal and movement to LayoutNode, clears with root.removeAll(), and calls root.owner?.onEndApplyChanges() from onEndChanges (UiApplier.android.kt). Android setContent constructs Composition(UiApplier(composeView.root), ...); subcompositions use the platform createApplier hook (Wrapper.android.kt).

The path is concrete: ComposeNode records factory/reuse plus updater blocks; ChangeList applies them; UiApplier changes LayoutNode children or properties. The next lesson follows the property write.

Reproduce the pinned create/update fixture

At the pinned checkout, run the exact runtime fixture:

./gradlew :compose:runtime:runtime:desktopTest \
  --tests 'androidx.compose.runtime.CompositionTests.composeNodeSetVsUpdate'

The fixture creates a SetUpdateNode and a mock SetUpdateNodeApplier, then calls composition.setContent with two real ComposeNode calls. The first uses set(value), the second update(value) (CompositionTests.kt#L3917-L3956).

Control: the initial state is "initial"; mutate it to "changed", send snapshot notifications, and advance the test scheduler. Expected assertions from the pinned method: both initial properties are "initial"; initial changeCount is 1 for set and 0 for update; after recomposition both properties are "changed"; counts are 2 and 1 (CompositionTests.kt#L3958-L3970). The Composables.kt source separately proves that insertion selects createNode(factory) and later composition selects useNode() (Composables.kt#L290-L315).

Limits: this fixture observes updater/property behavior, not a log of factory or useNode() calls, compiler lowering, or Android frame timing. This upstream procedure was not executed in this content edit.

Misconceptions

  • ComposeNode returns a LayoutNode.” It emits protocol calls; the applier owns the target object.
  • Updater.set mutates immediately.” It remembers a value and schedules an apply block.
  • SkippableUpdater means the node is skipped.” Only its composable update group can be skipped when valid; node identity and other updates remain separate.

Check yourself

A node’s label changes but its factory should not. Which part of this path should produce work: createNode, useNode, or an updater block?

Evidence ledger

ClaimDirect evidenceStatus
ComposeNode chooses create versus use and runs updater workComposables.kt#L290-L315Durable Version-specific
The pinned fixture composes two nodes and asserts set versus update resultsCompositionTests.kt#L3917-L3970Version-specific
Updater set/update/init/reconcile schedule apply work with different insertion semanticsComposer.ktVersion-specific
ComposeUiNode exposes LayoutNode update propertiesComposeUiNode.ktVersion-specific
Android uses UiApplier over a root LayoutNodeUiApplier.android.kt and Wrapper.android.ktVersion-specific

Freshness

Refresh when ComposeNode overloads, the compiler protocol, updater grouping, UiApplier, or LayoutNode construction changes. Do not treat ComposeUiNode’s internal interface as a general app-facing API.

Finished this lesson?

Your progress stays only in this browser.