Lesson 2
Invalidation can be conditional or imminent
Interpret InvalidationResult and see why a derived-state change or an active composition can alter the invalidation path.
Status: “Invalidated” is not one timing category. The four outcomes are Version-specific names for a Durable distinction between queued and active work.
Outcome
Interpret IGNORED, SCHEDULED, DEFERRED, or IMMINENT, and explain why a derived-state read can remain conditionally tracked.
Four outcomes
The pinned InvalidationResult enum defines these meanings (Composer.kt):
| Result | Meaning in the pinned runtime |
|---|---|
IGNORED | The scope is removed, not yet entered, or cannot recompose. |
SCHEDULED | The composition is not composing; the invalidation is recorded for later work. |
DEFERRED | Composition is active, but this scope was already processed or is being processed. |
IMMINENT | Composition is active and this scope is still ahead of the composer’s current position, so it will be visited during the same composition. |
CompositionImpl.invalidateChecked records the scope and tells the parent composition context that work exists. It returns DEFERRED while isComposing and SCHEDULED otherwise (Composition.kt). Before that point, invalidation can be IGNORED if the scope’s anchor is invalid or the scope does not belong to this composition.
Imminent means “ahead,” not “synchronous”
The gap-buffer composer’s tryImminentInvalidation resolves the scope anchor and compares its location with the reader’s current group. If composition is active and the scope is at or after the current position, the composer inserts the invalidation into its local invalidation map and returns true (GapComposer.kt). CompositionImpl translates that to IMMINENT. The link-buffer composer implements the same idea using a group address, but its storage path is still Experimental
at this snapshot (LinkComposer.kt).
It does not promise execution at the write site or an updated target node.
Conditional invalidation belongs to derived state
A scope becomes conditional when RecomposeScopeImpl records a derived-state value and dependencies. CompositionImpl.addPendingInvalidationsLocked keeps it in conditionallyInvalidatedScopes until draining, because a later value may invalidate it unconditionally (Composition.kt).
RecomposeScopeImpl.isInvalidFor compares the current derived value against the value recorded for that scope using the derived state’s mutation policy (RecomposeScopeImpl.kt). A dependency can change while the derived result stays equivalent. The scope then remains tracked but need not recompose for that notification.
This differs from an if statement’s replace group: it is a derived-state dependency decision, not structural branch reconciliation.
Reproduce the conditional case
This is a pinned upstream-test procedure, not an experiment run for this lesson. At AndroidX revision cc1caf65677fc10a8ce8116eba46e716f4cef222, inspect the pinned CompositionTests.kt and run:
./gradlew :compose:runtime:runtime:desktopTest \
--tests 'androidx.compose.runtime.CompositionTests.readingDerivedState_invalidatesWhenValueNotChanged'
Control: start with condition = false, so the derived value is 0; change it to true while state is still 0. Expected: expectNoChanges(). Increment state; the derived value changes and the test expects work. This bounds one policy and fixture, not every derived state or composer.
Moved content is another qualification
For moved content, CompositionImpl can delegate and later extract invalidations from the moved group (Composition.kt). Invalidation follows content ownership; the fields are versioned.
Check yourself
A scope is invalidated while composition is active, but its anchor is behind the current reader. Why is DEFERRED better than IMMINENT?
Evidence ledger
| Claim | Direct evidence | Status |
|---|---|---|
| The four invalidation outcomes have distinct active/queued meanings | Pinned Composer.kt | Version-specific |
| Ahead-of-reader scopes can be processed imminently | Pinned GapComposer.kt | Version-specific |
| Derived-state dependencies are conditionally retained and policy-checked | Pinned Composition.kt and RecomposeScopeImpl.kt | Durable
Version-specific |
| An equivalent derived result can produce no changes in the pinned test | CompositionTests.kt | Version-specific |
Freshness
Refresh after changes to InvalidationResult, anchor-to-reader comparisons, derived-state policy checks, or movable-content invalidation delegation. Keep “same composition” separate from “same frame” until Chapter 7 supplies the scheduler evidence.
Finished this lesson?
Your progress stays only in this browser.