Lesson 3
CompositionContext makes nesting one logical tree
Follow SubcomposeLayout from its parent context to child compositions created during measure.
Evidence legend: Durable marks a stable mental model; Version-specific marks behavior tied to the AndroidX pin below; Experimental marks a changing or transitional path.
A subcomposition is a separate Composition, but it should not become a separate universe. A child may need values from the parent, and a parent state write may need to invalidate the child. CompositionContext is the bridge: it preserves logical ownership while allowing a layout to calculate child content later than the parent content call.
The owner is a context, not a function call
The pinned runtime defines CompositionContext as an opaque reference to a parent composition at a specific tree position. Giving that reference to a child composition makes invalidations and CompositionLocal values flow logically as if the compositions were not separate; a root composition’s parent is the Recomposer (CompositionContext.kt#L28-L38). rememberCompositionContext() obtains the context at the current composition position. It is therefore an ownership handle, not a copy of all current values and not a replacement for the parent composition.
SubcomposeLayout captures that context during its own composition. Its ReusableComposeNode<LayoutNode, ...> update block stores the context in SubcomposeLayoutState; the state later passes it as parentComposition when it creates a child composition for a slot (SubcomposeLayout.kt#L125-L140, SubcomposeLayout.kt#L515-L519, SubcomposeLayout.kt#L666-L719). The child composition owns its slot content, but the parent context remains its logical route for invalidation and locals.
Why composition happens during measure
SubcomposeMeasureScope.subcompose(slotId, content) is specifically an operation in a measure or layout block. The implementation checks the LayoutNode state and rejects calls outside measuring, laying out, lookahead measuring, or lookahead laying out (SubcomposeLayout.kt#L152-L172, SubcomposeLayout.kt#L583-L596). This ordering lets measurement discover constraints or available space, then compose a slot that can be measured with those facts. It is not “recomposition in the middle of arbitrary Kotlin”; it is a constrained layout-phase child-composition operation.
A slotId is the identity used to match a child between measure passes. The public contract asks for an ID that compares equal to the previous ID; a list can use a stable item key, while a fixed set can use enum-like values. Calling the same ID twice in one pass is rejected, because the state cannot assign one slot identity to two active children (SubcomposeLayout.kt#L157-L172, SubcomposeLayout.kt#L596-L614). The slot ID is not automatically a movable-content identity: it selects a child composition owned by this layout state.
State proves the bridge
SubcomposeLayoutTest.compositionLocalChangeInMainCompositionRecomposesSubcomposition provides a useful control. The parent changes a CompositionLocal; the subcomposed lambda observes the new value, and the test checks that the child and main content agree. That is evidence for logical parent-child propagation, not evidence that every arbitrary global read is shared (SubcomposeLayoutTest.kt#L1048-L1099). The same file’s subcompose controls show that content can be composed during a measure pass and reused on the next pass (SubcomposeLayoutTest.kt#L138-L180).
Reproduce the boundary
At the pinned AndroidX checkout with an attached Android device or emulator, run the exact device-test class:
./gradlew :compose:ui:ui:connectedAndroidTest \
-Pandroidx.testInstrumentationRunnerArguments.class=androidx.compose.ui.layout.SubcomposeLayoutTest
Controls: subcompose a stable slot during measure, then change a parent CompositionLocal. Expected: the slot is available to measure, and the child observes the parent’s updated local without creating a second unrelated root. Limit: this device test does not prove all lookahead or intrinsic-measurement paths, and the command requires the AndroidX device-test configuration. This procedure was not executed in this content edit.
Misconceptions and check
- “Subcomposition means the child is independent.” It is a separate composition with a parent
CompositionContext; the logical owner still matters. - “The slot ID is a global state key.” It matches slots inside this
SubcomposeLayoutState; it does not replacekeyormovableContentOfeverywhere. - “Measure-time composition is unrestricted.” The pinned implementation enforces layout-phase entry points.
If the parent local changes while a slot remains active, expect the child to be invalidated through the context. If the slot ID changes to an unequal value, expect matching to fail and the layout’s reuse/disposal policy to decide what happens next.
Evidence ledger
| Claim | Evidence | Label |
|---|---|---|
| Context links parent and child invalidations/locals | CompositionContext.kt#L28-L38 | Durable |
SubcomposeLayout stores and supplies its parent context | SubcomposeLayout.kt#L125-L140 | Version-specific |
subcompose is restricted to layout phases and matches slot IDs | SubcomposeLayout.kt#L583-L614 | Version-specific |
Freshness
Refresh when CompositionContext propagation, parent invalidation registration, measure-phase restrictions, or SubcomposeLayout slot matching changes. Keep child-composition behavior bounded to the pinned UI implementation and tests.
Finished this lesson?
Your progress stays only in this browser.