Lesson 4

Composition calculates, then applies changes

Follow pending operations from composition calculation to an applier-owned target tree.

3 min readUpdated Jul 11, 2026

Status: The calculation/application split is Durable . Method names and callback ordering are Version-specific at the pinned revision.

Outcome

You should be able to explain why a successful composition calculation does not mean that the target tree has already changed.

Two phases, two questions

Composition asks:

What operations should happen for this execution?

Application asks:

How should those operations mutate the target tree?

The pinned ControlledComposition makes this boundary explicit. composeContent and recompose calculate changes. applyChanges performs them. Its contract says that tree changes have not happened before application (Composition.kt, ControlledComposition).

Diagram
flowchart TD
    Compose[Compose content] --> Calculate[Calculate changes]
    Calculate --> Pending[Pending operations]
    Pending --> Apply[applyChanges]
    Apply --> Applier[Applier calls]
    Applier --> Tree[Target tree]

The change list is an intermediate result. It is not the target tree either.

The applier owns tree mutations

Applier<N> defines the target-tree boundary. Its operations include moving the current parent, inserting, removing, moving, clearing, and applying a property block.

The interface is generic. A composer can maintain a tree that is not a Compose UI tree. Compose UI supplies its own bridge later.

This ownership answers a common debugging question:

  • If composition calculated a change but the target has not changed, inspect the pending operations and apply phase.
  • If the target changed incorrectly, inspect the applier contract and its implementation.

Do not collapse both questions into “the composable ran.”

Application is also a lifecycle boundary

The runtime brackets change execution with applier.onBeginChanges() and applier.onEndChanges(). It also dispatches remembered-object callbacks and recorded side effects around the apply work (Composition.kt, applyChangesInLocked).

That ordering explains SideEffect. The composable body records the effect through Composer.recordSideEffect. The runtime runs it after the calculated changes have been applied. It is not simply an arbitrary line executed during tree mutation.

This is a Version-specific ordering claim. Use the pinned implementation and tests when exact callback order matters.

A controlled prediction

Imagine a composition that calculates one node insertion.

Before applyChanges:

  • composition memory may contain the new group;
  • a change operation may be pending; and
  • the applier’s target tree should still have its old child list.

After applyChanges:

  • the applier receives the insertion;
  • the target tree reflects the new child; and
  • apply-phase callbacks can run.

That is the experiment to reproduce with a logging applier. Count operations and inspect the tree before and after application. Do not use log timing as evidence.

Check yourself

Why can recompose() return useful work while the UI node tree still shows the old value? Name the method that crosses the boundary.

Source notes

ClaimDirect evidenceStatus
Composition calculates before applyChanges mutates the treeComposition.kt, ControlledCompositionDurable Version-specific
An applier defines insertion, removal, movement, and property operationsApplier.ktDurable
Change application brackets callbacks and side effectsComposition.kt and Composer.ktVersion-specific

Freshness

Refresh this lesson when ControlledComposition, applyChangesInLocked, recordSideEffect, or the applier contract changes. Recheck the runtime tests before making a stronger ordering claim.

Finished this lesson?

Your progress stays only in this browser.