Lesson 4
Composition calculates, then applies changes
Follow pending operations from composition calculation to an applier-owned target tree.
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).
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
| Claim | Direct evidence | Status |
|---|---|---|
Composition calculates before applyChanges mutates the tree | Composition.kt, ControlledComposition | Durable
Version-specific |
| An applier defines insertion, removal, movement, and property operations | Applier.kt | Durable |
| Change application brackets callbacks and side effects | Composition.kt and Composer.kt | Version-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.