Delayed Session Container Checklist
1. Scope
This document tracks stability risks in containers that combine delayed creation with holder/session reuse.
These containers share three properties:
- Content is not mounted under the parent immediately.
- Holders or sessions are reused internally.
- Structural diffing can be decoupled from visible-content refresh.
They are therefore high-risk areas for stale content when structure remains unchanged.
2. Current containers
LazyColumnLazyRowLazyVerticalGridHorizontalPagerVerticalPager- navigation destination pages, where content is carried by
NavDestinationSession
TabRow is deliberately excluded: its small, resident tab set is rendered as ordinary eager keyed
children in the parent composition.
3. Hard architecture constraints
Every delayed-session container must satisfy these constraints:
-
An empty diff must not fall back to an old item or page instance.
-
Equal key plus content/environment revisions must skip the item session completely.
-
The update path reinjects
localSnapshot, theme, environment, and the latest parent closure. -
A delayed create path may prepare a native child tree, but only activation or an active update can cross the child composition/effect commit boundary.
-
activatehappens at most once; laterrenderoperations apply active submissions, anddispose/recyclesemantics align with holder lifetime. -
A
Changeupdate prefers the payload path instead of an unconditional full-change signal. -
When unusable keys force
ReloadAll, preserve the current scroll anchor where possible instead of jumping the collection to the top after an interaction. -
Focusing an input must not cause an unrelated list jump. A real vertical scroll owner always preserves Android's child-rectangle protocol and may move only enough to reveal the focused editor while preserving the logical item anchor; there is no container opt-in policy.
-
A parent collection submission is one monotonic child-session revision. Its retained-child updates publish only from the parent render frame's commit effects, after composition commit; parent rollback discards them without running child composition or effects.
-
Strategy or payload identity is not a revision. A changed ordinary capture must be State or participate in
contentRevision; allocation alone never refreshes content. Single item, sticky-header, page, and tab declarations require a non-null revision immediately afterkey; optional physical-reuse and layout arguments follow it.nullis not a sentinel.StaticContentRevisionpromises that no such ordinary input changes, while the nullable bulk{ it }default is limited to immutable value models whose equality covers every ordinary input read by item content. Each independently composed lazy item, sticky header, or pager page emits exactly one root VNode. Its physical holder has one measurement and placement boundary and does not infer whether siblings should stack, line up, or overlay. Zero or multiple roots fail during composition preparation and roll back before the renderer sees the candidate; callers express sibling layout with an explicitColumn,Row, orBoxand useSpacerfor an empty entry. -
Every ordinary typed
Listdeclaration reevaluates order, membership, and itskey,contentType,contentRevision, and grid-span selectors on each parent composition pass. The collector may reuse an already committed logical item only when its key, content revision, environment, content type, kind, and span are all equal. It retains the committed ordered list plus at most one previous semantic variant for each current key; when candidate order contains the same item identities at every position,buildreturns that committed list instance. Homogeneous top-level andScrollableScopecontainers may instead receive aLazyItemsSnapshot. Its factory shallow-copies ordered item references and allocates an opaque identity without evaluating selectors. Each collector retains the current and immediately previous successfully committed evaluated snapshot, keyed by exact source identity plus framework environment. An exact hit restores the ordered list and key map in constant time without selectors, a key scan, or per-item callback wrappers. One typed declaration shares aLazyListItemSessionStrategyand stores each source model as the selected item's opaque payload; Holder create/update consumes that payload synchronously without retaining the item snapshot or allocating a bind-time content closure. An environment mismatch reevaluates every selector. Scoped declarations have no snapshot overload. Only State read while item content executes in its active Session remains independently observed. State or another changing input read by a selector requires a replacementLazyItemsSnapshot, as do order, membership, retained item data, selector-capture, and ordinary item-content-capture changes.The unique miss traversal precomputes displaced variants, reverse variants needed to restore the previous snapshot, and key-membership deltas. Only the successful parent commit's
SideEffectpublishes an evaluated snapshot and its cache state. Selector or duplicate-key failure publishes nothing, so retry reevaluates every selector. If a delayed side effect finds that the cache generation has advanced, it recomputes membership and both variant directions against the current committed generation instead of publishing stale precomputation. A parent rollback never publishes candidate item bindings. ViewCompose does not accept a raw aggregate caller token that can bypass ordinaryListchecks. -
A detached, never-activated holder may prepare a committed parent submission without running remember activation, effects, native commit callbacks, overlays, or committed diagnostics. Activation commits a valid candidate without rebuilding it. An already-active detached holder stages the latest revision and renders it on reattach; ambiguous duplicate keys never use first-match lookup to guess ownership.
-
Pager stable IDs are collision-free for unique keys, and native view types partition structurally incompatible
contentType/kind pairs. Unkeyed cached pages retain position ownership; keyed moves resolve only through a unique key in both snapshots. The RecyclerView pager viewport treats only a real settled transition as selection, so an idle relayout cannot clear focus from the current page. Within-page focus visibility belongs to a page-local scroll owner and stops before the pager boundary. -
Every independently composed item/page receives a child
SaveableStateRegistryowned by a parent-composition holder and its stable logical key. Recycling retains that registry's saved map, reordering follows the key, and nested containers repeat the hierarchy. -
A renderer-created concurrent presentation replica may restore the logical owner's current saveable snapshot but must not register a second persistence owner for the same logical key.
-
Recycling ends the logical key session before physical reset. Compatible mounted trees live only in a framework-owned, bounded cache with deterministic eviction; native pools retain empty holder shells.
-
AndroidViewparticipates in cross-key reuse only withonReset; final eviction callsonReleaseexactly once.
4. Required scenarios
Every container covers at least these eight cases:
- Stable structure, changed closure but equal revisions: no item render occurs; changing content without State requires an explicit revision change.
- Stable structure, changed local context: theme, Local, or environment changes become visible.
- Changed
contentRevision: reuse or controlled recreation follows the documented semantics. - Keyed reorder: ordering is correct and state does not move between items.
- Prepare/attach/detach/recycle: a never-activated cache runs no child commit work, attach presents the latest committed revision, active detach does not restart lifecycle work, and recycle leaks no state.
- Empty-diff submission: attached holders perform no item render or native patch.
- Failed parent frame: retained child update/render/effects do not run.
- Duplicate low-level item keys: conservative reload avoids guessed holder identity; public DSLs reject missing or duplicate keys while building the snapshot.
- Saveable-state ownership: sibling local keys do not collide, keyed recycling/restoration does not move state, and presentation replicas cannot overwrite the logical owner.
- Cross-key physical reuse: old effects dispose before reset, new logical state starts empty, failed rebind cannot call old updater callbacks, and eviction releases exactly once.
5. Current test mapping (2026-08)
Foundation unit tests:
- TypedLazyCollectionContractTest.kt
- LazyListDiffTest.kt
- LazyHolderRegistryTest.kt
- LazyItemSessionControllerTest.kt
- LazyListAdapterTest.kt
- ViewTreeRenderTransactionTest.kt
- PagerAdapterTest.kt
Covered special cases:
LazyColumn:collectionsStress_toggleUpdatesVisibleControls(UI)LazyVerticalGrid:collectionsGrid_spanToggle_refreshesVisibleItemContent(UI)TabRow + HorizontalPager: eager keyed tab state and pager revision cases (UI)HorizontalPager:statePatchStress_horizontalPagerContentUpdatesAcrossExplicitRevisions(UI)VerticalPager:statePatchStress_verticalPagerContentUpdatesAcrossExplicitRevisions(UI)LazyVerticalGrid/HorizontalPager/VerticalPager: collection patch cases inNodeBindingDifferTest(unit)LazyColumn:collectionsStress_rotateOrder_refreshesVisibleIdsAcrossToggles(UI)- Navigation destinations:
NavDestinationSessionStoreTestcovers candidate off-screen first render, failed rollback, Local/content-closure refresh, visibility layers, permanent removal, and owner release (unit). - Transactional navigation host:
TransactionalNavHostCoordinatorTestcovers attach, push/pop/replace/reset, revealed-page refresh failure, initial-failure retry, serialized reentrancy, and lifecycle caps (unit). - Public navigation: the
:samples:tutorialsdevice test covers push and Back through the productionNavHost(instrumentation).
Current baseline notes:
qaFullremains the connected-device gate for application behavior.- Since 2026-03-07, Lazy/Pager uses the unified DiffUtil plus payload
Changepath while preserving empty-diff refresh semantics. - Since 2026-07-26, candidate navigation pages commit their first frame off-screen, committed
pages refresh the latest
UiLocalSnapshotand content closure, and rollback/removal releases session before owner. - Since 2026-07-26, a back-stack commit occurs only after candidate first render or revealed-page refresh succeeds. Reentrant commands created by a failed candidate do not leak into the old stack.
- Since 2026-08-12, lazy and pager child submissions join the parent commit-effect boundary. Attached holders render once per explicit submission revision; detached caches and rolled-back parent frames run no child render or effects.
- Pager moves proactively refresh attached uniquely keyed pages after commit. Hash-colliding keys keep distinct stable IDs, and unkeyed detached pages resolve the committed snapshot by their bound position when reattached.
- Since 2026-08-13, a never-activated lazy holder uses the Prepared → Active → Disposed protocol. RecyclerView prefetch can build its composition and native tree before attachment, while the existing transaction defers remember activation, effects, native commit work, overlays, and diagnostics. An observed state change invalidates the candidate before activation.
- Since 2026-08-14, item/page snapshots use caller-owned content revision plus framework-owned environment revision. Equal revisions skip child rendering; changed revisions target one item.
- Since 2026-08-14, logical sessions and physical mounted trees have separate ownership. TabRow uses eager keyed children; resettable trees may cross lazy keys only through the bounded renderer-owned cache.
- Since 2026-08-16, ordinary
Listdeclarations retain per-pass selector validation, while the explicitLazyItemsSnapshotpath provides a bounded two-generation exact-identity fast path for homogeneous list, row, and grid overloads. Environment changes and snapshot replacement return to selector evaluation; scoped declarations remain on the ordinary safe path.
6. New-container workflow
Adding a delayed-session container requires all of the following:
- register the container in the architecture overview;
- add it to this checklist with a test mapping;
- add unit cases for equal-revision skip, explicit-revision update, parent rollback, and detached-holder attach;
- add real Activity instrumentation;
- confirm that render/layout diagnostics expose the behavior.
7. Investigation order
For stale text, misplaced state, or an outdated page, investigate in this order:
- determine whether the content is inside a delayed-session container;
- determine whether a parent commit effect published the latest item/page submission revision;
- determine whether the holder was attached, detached-cached, or ambiguously keyed;
- determine whether the holder rendered that revision exactly once;
- only then inspect the Demo application code.