Skip to main content

Navigation runtime architecture

1. Ownership boundary

ViewCompose navigation uses an Activity or Window as the outer Android host, but a destination is a framework-owned page rather than an Activity or Fragment. The capability is split across two published artifacts:

  • viewcompose-navigation-core owns the platform-neutral route, graph, retained-stack, transaction, lifecycle-plan, deep-link, and pane-scene models;
  • viewcompose-navigation-android owns destination and graph Android owners, child render sessions, native View presentation, SavedState encoding, system and predictive Back, and visual motion.

The split keeps Android ownership out of the state machine while giving the native host one place to coordinate stack state, rendering, lifecycle, and View hierarchy changes.

2. Transaction boundary

Navigation is a two-phase operation. Core prepare calculates an immutable candidate state and entry mutation without publishing it. The Android host prepares the destination owner and child render session, renders into a staged native container, and only then commits the core transaction. Preparation failure rolls back the candidate and preserves the old stack, visible scene, and owners.

Only one prepared transaction may exist for a controller. Re-entrant commands received during render, lifecycle movement, or visual motion are serialized after the current operation reaches a terminal state. NavResult.Queued therefore means accepted pending work, not committed completion.

After the stack commits, visual motion may be completed, cancelled, or redirected, but it cannot undo application state. Every terminal visual path settles on the committed target. A failure in a post-commit effect is reported with stackCommitted = true; the host never pretends that the old stack is still authoritative.

3. Destination and graph identity

Every destination entry owns a stable child render session, Lifecycle, ViewModelStore, SavedStateRegistry namespace, and ViewCompose saveable-state namespace. Pushing the same route twice creates two entry identities. Hidden retained entries keep their identity and stored state, but frame-driven work is capped by lifecycle and rendering resumes before the page becomes visible again.

Nested graph instances have independent NavGraphOwner identities. Descendants in one graph instance share its lifecycle, saved state, and ViewModels until the last retained descendant is removed. Entering the same graph route later creates a new owner. The root-to-leaf graph chain is available only while rendering destination content; it cannot be used to manufacture ownership outside the active host.

The nearest parent ViewModelStore owner supplies its default factory and creation extras. A child navigation owner replaces only the store owner, saved-state owner, and route or graph arguments. Changing the parent owner identity recreates the native host so retained entries never combine two provider contracts.

4. Lifecycle projection

The host projects committed navigation and pane state into Android lifecycles, capped by the outer host lifecycle:

RoleTarget state
Interactive settled destination and its graph pathRESUMED
Visible transition participantSTARTED
Retained hidden destination or graphCREATED
Prepared candidate before commitNo higher than CREATED
Permanently removed destination or graphDESTROYED

Downward transitions happen before upward transitions, so a single-pane host never briefly owns two resumed destinations. A validated multi-pane scene may resume multiple leaf destinations and their shared graph paths intentionally. Destroyed entry and graph identities cannot be reintroduced.

5. Restoration boundary

Remembered controllers persist committed stacks, route arguments, destination and graph identities, selection history, destination and graph SavedStateRegistry bundles, and ViewCompose saveable values. They do not serialize Views, render sessions, LifecycleRegistry instances, ViewModelStore contents, pending transactions, or running animations.

Restore validates format limits, stack configuration, route existence, leaf resolution, and graph hierarchy. Incompatible or malformed state is discarded and the configured initial state is used. Failing closed prevents an old SavedState or ViewModel namespace from being attached to a different destination after an application update.

6. Back and visual motion

System Back participates only while the active controller can consume it. Predictive Back creates a preview over committed entries without changing the core stack. Cancellation restores the settled scene; completion uses the normal pop transaction. Detach, disabled Back, or host destruction cancels an unfinished preview because the platform dispatcher may no longer deliver a terminal callback.

NavTransitionSpec and shared-content capture are presentation policy. They operate on already owned destination roots after commit, own no page/session retention, and cannot receive input or accessibility focus. Capture failure degrades the affected visual pair without changing navigation state.

7. Evidence and verification

The invariant boundary is covered at three levels:

  • Navigation Core tests exercise two-phase transactions, deterministic retained stacks, graph validation, strict deep links, lifecycle plans, and pane-scene validation.
  • Navigation Android tests exercise candidate rollback, retained owner identity, lifecycle order, SavedState compatibility, queued commands, transition redirection, and predictive Back.
  • The compiled navigation tutorial and the production-host guide provide the public first-success and manual acceptance paths.

Run ./gradlew :viewcompose-navigation-core:test :viewcompose-navigation-android:testDebugUnitTest for the deterministic architecture suite. Device behavior is accepted only when the guide's real Back, recreation, predictive-Back, and failure journey also passes.