Lesson 2

Reusable composition changes the node lifecycle

Distinguish node recycling from movable identity and follow reuse, deactivation, and release callbacks.

4 min readUpdated Jul 12, 2026

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.

Movable content answers “how do I keep this identity while its location changes?” Reuse answers “how do I avoid allocating a new compatible node for different content?” The second question deliberately has a different answer for remembered values: node structure may be recycled, while remembered slots are treated as invalid for the new content.

Two reusable layers

ReusableContent(key) { ... } starts a reusable group. When key changes, cached values act invalid, changed behaves as changed, and the composer seeks ReusableComposeNode nodes to update instead of replacing (Composables.kt#L130-L146, Composer.kt#L500-L520). ReusableComposeNode marks this protocol with startReusableNode() before factory/update work (Composables.kt#L318-L337). Ordinary ComposeNode is not automatically recyclable; a non-reusable node forces replacement for itself and its children.

ReusableContentHost(active, content) is the lifetime gate. When inactive, it calls deactivateToEndGroup: cached slot values are cleared while applier-managed nodes remain in place (Composables.kt#L149-L174, Composer.kt#L435-L445). Reactivation composes again and makes reusable nodes candidates. Deactivation is not a hidden remember extension: it forgets remembered data and observations while keeping a physical node available.

The public ReusableComposition interface gives a child-composition owner the same distinction. setContentWithReuse discards remembered content and reuses eligible nodes when group structure matches; deactivate removes observation scopes and remembered slots while preserving nodes (Composition.kt#L92-L121). Normal Composition.dispose() ends the composition and releases its hierarchy. Runtime release history places ReusableContent/ReusableComposeNode and later ReusableComposition.deactivate in separate additions (Compose Runtime 1.6.0-alpha03, 1.6.0-alpha04).

Lifecycle callbacks are not remember callbacks

Nodes implementing ComposeNodeLifecycleCallback can observe three intermediate states. onReuse resets data that belonged to prior content; onDeactivate releases resources that depend on current composition use while leaving the node eligible for later reuse; onRelease is terminal for that node (ComposeNodeLifecycleCallback.kt#L30-L60). RememberObserver is insufficient for data associated with a recycled node because deactivated or reused remembered parts are disposed while the node remains available.

The pinned CompositionReusingTests make the split observable: canReuse asserts identical node objects but a different remembered object after a key change; reusableContentHostCanDeactivate observes remembered state forgotten while inactive; onReleaseIsNotCalledOnReuse and onReleaseIsNotCalledWithMovableContentMovement keep recycling and moving separate from terminal release (CompositionReusingTests.kt#L44-L110, CompositionReusingTests.kt#L207-L263, CompositionReusingTests.kt#L529-L645).

MechanismPrimary promiseRemembered valuesPhysical nodes
movableContentOfPreserve one content identity at a new call siteTransfer with the contentTransfer with the content
ReusableContentRecycle compatible node groups for a changed keyTreat as invalid/newReuse eligible nodes
ReusableContentHost(false)Temporarily deactivate a reusable regionForget observations and slotsKeep nodes in place
dispose / ordinary removalEnd ownershipReleaseRemove/release

Reproduce the boundary

At the pinned AndroidX checkout, run:

./gradlew :compose:runtime:runtime:desktopTest \
  --tests 'androidx.compose.runtime.CompositionReusingTests.canReuse' \
  --tests 'androidx.compose.runtime.CompositionReusingTests.reusableContentHostCanDeactivate' \
  --tests 'androidx.compose.runtime.CompositionReusingTests.onReleaseIsNotCalledOnReuse' \
  --tests 'androidx.compose.runtime.CompositionReusingTests.onReleaseIsNotCalledWithMovableContentMovement'

Controls: change a reusable key, toggle host activity, remove content, and move the same movable content. Expected: reuse keeps eligible node identity but not the remembered object; deactivation forgets state without release; removal eventually invokes release. Limit: runtime mock tests do not define a UI toolkit’s resource reset or layout policy. This procedure was not executed in this content edit.

Check yourself

A ReusableContent key changes while its group emits one ReusableComposeNode and one ordinary node. Predict the result: the reusable node is a candidate for update, the ordinary node is replaced, and remembered values are not a cache that survives the recycling transition. If the requirement instead says “the exact remembered editor follows a new branch,” use movable content, not reusable content.

Evidence ledger

ClaimEvidenceLabel
Reusable groups invalidate cached values but seek reusable nodesComposer.kt#L466-L520Version-specific
Deactivation preserves nodes while clearing remembered slotsComposition.kt#L97-L121Version-specific
Tests distinguish reuse, deactivate, move, and releaseCompositionReusingTests.kt#L529-L645Version-specific

Misconceptions

  • “Reusable content preserves remembered state.” Reuse targets eligible physical nodes; the remembered slots are treated as invalid for the new content.
  • “Deactivation releases the node.” Deactivation forgets composition-owned values and observations while leaving an eligible node in place; release is terminal.
  • “Movable content and reusable content solve the same problem.” Movable content transfers one identity; reuse recycles a compatible node for different content.

Freshness

Refresh when reusable-group entry, deactivation, ComposeNodeLifecycleCallback, ReusableComposition, or release semantics change. Keep node reuse separate from remembered-value lifetime and movable-content transfer.

Finished this lesson?

Your progress stays only in this browser.