Navigation Android
viewcompose-navigation-android mounts viewcompose-navigation-core state as native Android View pages. It
owns destination and graph lifecycle boundaries, ViewModel stores, SavedStateRegistry namespaces,
child render sessions, transactional failure recovery, Android system and predictive Back,
adaptive pane layout, and command-aware View motion.
The application still uses an Activity or Window as its outer Android host, but individual pages do
not require an Activity or Fragment. The platform-neutral back stack remains in
viewcompose-navigation-core; this module is its Android execution boundary.
Artifact and stability
dependencies {
implementation("com.viewcompose:viewcompose-navigation-android:0.1.0-alpha01")
}
- Stability: Alpha. Host, transition, and predictive-Back contracts may evolve between alphas.
- Platform: Android library with a minimum SDK inherited from the repository Android policy.
- API dependencies are Navigation Core, Runtime, UI Contract, and UI Foundation because their route, state, node, and builder types form the public navigation surface.
- Implementation dependencies are Android Host, Lifecycle, ViewModel integration, and the neutral Android overlay transport. Android Renderer arrives privately through Android Host and is not a direct dependency of this artifact.
- The artifact transitively supplies
viewcompose-navigation-core; applications may depend on the core artifact alone when they need only the platform-neutral model.
Controller and host
fun UiTreeBuilder.AppNavigation() {
val controller = rememberNavHostController(
startDestination = NavRoute("home"),
)
NavHost(controller = controller) { entry ->
when (entry.route.name) {
"home" -> HomePage(controller)
"details" -> DetailsPage(controller)
else -> error("Unknown route ${entry.route.name}")
}
}
}
One NavHostController can be attached to exactly one active NavHost. Navigation commands are
main-thread APIs and require attachment so the core transaction, destination rendering, owner
lifecycle, and native View hierarchy share one commit boundary.
NavHost creates one retained child render session per destination. Hidden stack entries keep their
session and owners but pause frame-driven rendering. The host updates their captured environment
without eagerly rendering every retained page. Immediately before a retained destination newly
enters the visible pane set through pop, stack selection/history, predictive Back, or adaptive-pane
expansion, that same session renders against the latest environment. Newly prepared destinations
are not rendered twice.
Each destination session receives the NavigationDestination diagnostics role and the parent
session ID captured with the NavHost Local snapshot. Retention preserves that logical identity;
failed candidates emit their own terminal sequence, while recreated destinations receive a fresh
ID. Restoring destination Locals cannot overwrite the child session owner.
Change contentKey when destination content closes over non-observable values. Observable state
invalidates its owning destination session directly. Changing key, controller identity, lifecycle
owner, debug identity, or overlay factory recreates the native host because those inputs change
ownership rather than content.
The default nested overlay factory explicitly constructs viewcompose-overlay-android; it never
discovers a Material backend from classpath order. A named design integration may pass an explicit
factory when its destination surfaces require additional presenters.
Command results and re-entrancy
Controller commands return NavResult:
Committedreports the state and entry-owner mutation applied by the host;NoChangereports a valid command that was already effective;Queuedmeans a transition or callback is active and the command will run serially later;Failedreports structured render or commit context.
Destination callbacks may navigate synchronously while another render, lifecycle update, or motion
completion is in progress. The host queues those re-entrant commands and drains them only after the
current operation reaches a terminal state. A queued result is therefore not completion; observe
controller.navigationState for the eventual committed multi-stack state.
The controller exposes immediate immutable snapshot and stackState projections plus observable
navigationState. Selected-tab UI should derive its selection from activeStackId rather than
maintaining a second source of truth.
Destination and graph ownership
Every destination entry receives an independent Android owner containing:
- a Lifecycle capped by the host and pane visibility;
- a ViewModelStore cleared only after the entry leaves all retained state;
- a SavedStateRegistry and default SavedStateHandle arguments derived from
NavRoute; - a ViewCompose saveable-state registry namespace.
Destination content installs that object into LocalLifecycleOwner,
LocalSavedStateRegistryOwner, LocalViewModelStoreOwner, and the ViewCompose saveable-state local.
Graph content installs the selected graph owner through the same four boundaries. A retained hidden
destination keeps its owner identity and persisted data but receives a capped lifecycle, so a
LifecycleAndroidViewAdapter drives its native View inactive without relying on physical removal.
Pushing the same route twice creates two owners and does not share page state.
At native NavHost creation, destination and graph owners capture the nearest
LocalViewModelStoreOwner. If it implements HasDefaultViewModelProviderFactory, its default
Factory and starting CreationExtras are inherited. Each child owner then replaces only the
ViewModelStore owner, saved-state owner, and route or graph default arguments, preserving unrelated
Application and DI extras. A different parent-owner identity recreates the native host; retained
stacks therefore never mix provider contracts from two parents.
Nested graph instances receive NavGraphOwner boundaries. Destinations in one graph instance share
its Lifecycle, ViewModelStore, and SavedStateRegistry until the last descendant leaves the stack.
Entering the same graph route again later creates a new owner.
LocalNavGraphOwnerScope.current exposes the active root-to-leaf owner chain. Use
ProvideNavGraphOwner(route) around a subtree that should resolve lifecycle, ViewModels, and saved
state against a graph rather than the leaf destination. It fails when called outside destination
content or for an inactive graph route.
Failure and rollback
The Android host preserves the two-phase guarantee from navigation core. New destination sessions
and owners are prepared first, then staged into the View hierarchy, then pure stack state commits,
and finally commit effects run. Failures are classified by NavFailurePhase.
NavFailure.stackCommitted distinguishes failures before and after the irreversible stack boundary.
Before commit, candidate sessions and owners are removed and the core transaction rolls back. After
commit, the host keeps committed state and reports the effect failure rather than pretending that
the previous stack is still authoritative.
A retained-page render that fails before reveal is reported as DestinationRefresh with
stackCommitted = false. The previous stack, pane scene, visible Views, owners, and sessions remain
authoritative; predictive previews and pane expansion are not published.
Pass onFailure to NavHost for application logging, fallback, or tests. An unhandled failure is
surfaced as NavHostException with the original cause, failed entry, and renderer frame report.
Save, restore, and process death
rememberNavHostController uses the current ViewCompose saveable-state registry. It persists:
- every retained stack, active stack, and selection history;
- destination and graph instance IDs and route arguments;
- destination and graph SavedStateRegistry bundles;
- ViewCompose saveable values owned by each page or graph.
Pending transactions, running animations, Views, sessions, LifecycleRegistry instances, and ViewModelStore contents are not serialized.
Restore is defensive. Unknown versions, malformed collection types, excessive entry counts, configuration mismatch, or graph hierarchy changes discard incompatible saved state and create the configured initial state. This fail-closed behavior avoids attaching an old saved-state namespace to a different page owner after an application upgrade.
Android system and predictive Back
With systemBackEnabled = true, NavHost registers against the nearest AndroidX Back dispatcher
only while the controller can consume Back. At the active root it follows the configured retained
stack history; otherwise dispatch continues to an enclosing host or Activity.
On predictive-Back platforms, gesture progress reveals the previous destination without committing
the core stack. Cancellation springs both pages back to committed state. Gesture completion uses the
same transaction and owner boundary as programmatic popBackStack. A programmatic command can
redirect an active preview while preserving its current visual transform for a continuous handoff.
Detaching the View, disabling system Back, or destroying the host actively cancels an unfinished preview because the dispatcher may no longer send a terminal callback.
Motion
NavTransitionSpec is visual policy only; changing it never mutates navigation state or ownership.
It independently configures push, pop, replace, reset, stack selection, deep-link, and predictive-
Back motion.
NavDestinationTransform combines pane-relative travel, dp travel, alpha, and scale. Geometry and
incoming/outgoing alpha can use independent durations, delays, and NavMotionEasing curves. The
default push/pop geometry and emphasized easing are aligned with current Android activity motion;
predictive Back follows current WM Shell cross-activity geometry. Use NavTransitionSpec.None when
the application or test must disable all motion.
The View driver renders complete starting layouts before starting motion and temporarily promotes expensive destination hierarchies to hardware layers while only transform/alpha changes. Redirected motion retains current visual properties so a subsequent command does not jump back to an identity frame.
Shared content motion
Modifier.sharedElement(SharedContentKey(...)) and Modifier.sharedBounds(...) are Q3 endpoint
markers consumed automatically by NavHost; no SharedTransitionLayout or animation scope is
required. Keys are local to one outgoing/incoming destination pair. A pair exists only when each
tree declares the same key and mode exactly once. Missing, duplicate, mismatched, detached,
zero-sized, surface-backed, or over-budget endpoints fall back per key to ordinary destination
motion and never change the navigation transaction.
The first release is one-window snapshot motion. sharedElement moves the source snapshot to the
target bounds. sharedBounds uses the same bounds path while crossfading source and target
snapshots. Snapshots draw in stable outgoing-tree order in a non-interactive host overlay and are
bounded to at most two host areas of pixels for one transition. The incoming destination remains
the input and accessibility owner. Successful commit may transfer focus from a focused source to a
focusable target; cancellation restores the source. Completion, cancellation, redirect, host
destruction, capture failure, and session release all remove snapshots and restore endpoint state
exactly once. Predictive Back drives the same overlay from gesture progress and continues from that
fraction on commit; it does not acquire stack commit authority.
Adaptive panes
NavPanePolicy.Single preserves one full-host destination at every width. Adaptive admits up to
three newest entries when each pane can retain the configured minimum width. paneSpacingDp is
deducted before deciding how many panes fit.
Width changes reuse the committed back stack, destination sessions, and owners. They refresh only retained entries newly admitted to the pane scene before recalculating native child bounds. Layout direction maps primary-to-tertiary roles to the correct physical order for LTR and RTL.
Deep links and retained stacks
The string, Android Uri, and ACTION_VIEW Intent entry points all use the same strict graph
resolver. A match is converted to one atomic command that updates the declared target stack and
selects it. NavDeepLinkResult.Navigated still contains a NavResult, so rendering or commit failure
is not confused with URI matching success.
For multiple tabs, declare one NavStackConfiguration and remember it with the shared graph. Do not
create one controller per tab or mirror active-stack state in application fields; the controller
already retains each stack and owns selection history.
Related documentation
- Navigation core module
- Complete navigation guide
- Lifecycle and saved-state architecture
- Session container architecture
- Source documentation and API comment standard
The complete generated reference is available in the
viewcompose-navigation-android API tree.
Compatibility notes
The 0.1.0-alpha01 line establishes one-controller/one-host attachment, main-thread serialized
commands, destination and graph ownership, defensive process-death restore, predictive-Back
preview, Android-aligned native View motion, and up to three adaptive panes. Persist controller state
through rememberNavHostController; do not retain Android owner or session objects outside the host.
Typed shared-content markers are additive Q3 UI Contract APIs, but they require a renderer that publishes the stable endpoint tag and this navigation-host implementation to produce motion. Older or custom renderers may treat the marker as inert. Cross-window, cross-Activity, cross-process, live-content, shape-morphing, and arbitrary surface-backed capture are intentionally unsupported in this alpha and use ordinary destination motion.