Navigation Android
viewcompose-navigation-android mounts viewcompose-navigation-core state as native Android View pages. It
owns destination and graph lifecycle boundaries, scoped ViewModel owner leases, SavedStateRegistry
namespaces, policy-bound child render sessions, transactional failure recovery, Android system and
predictive Back, adaptive content panes, modal overlay scenes, 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-alpha02")
}
- 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.
- Runtime-only dependencies include the Android integrations, Activity Back fallback, and direct
NavigationEvent 1.1.2 input. Android Renderer stays private through Android Host, while
navigationevent-testingstays test-only. - 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}")
}
}
}
Custom overlay transports are constructor inputs. Keep their factory reference stable across ordinary renders and advance an explicit key only when the transport must be rebuilt:
fun UiTreeBuilder.customOverlayNavHostSample(
controller: NavHostController,
overlayHostFactory: (ViewGroup) -> OverlayHost,
overlayFactoryVersion: Any,
) {
NavHost(
controller = controller,
overlayHostFactory = overlayHostFactory,
key = overlayFactoryVersion,
) { entry ->
Text(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.
Typed commands
data class ArticleRoute(val articleId: Long)
fun typedRouteNavigationSample(
controller: NavHostController,
destination: NavRouteSpec<ArticleRoute>,
): ArticleRoute {
controller.navigate(destination, ArticleRoute(articleId = 42L))
return controller.snapshot.top.toRoute(destination)
}
Use the same NavRouteSpec<T> with graph declarations, navigate, replaceTop, reset, and
NavEntry.toRoute. Encoding runs on the main thread before a host transaction begins, so an
encoder exception cannot mutate the stack, render tree, owner lifecycle, or result inbox. The
controller and saved-state adapter still receive only NavRoute; no live route object or callback
is retained.
NavHost retains logical owners independently from native presentations. Missing visible
presentations are rebuilt against the latest environment before a scene is published; failure
disposes candidates and preserves the committed stack. Change contentKey for non-observable
content inputs, and change the host key when an ownership input or overlay factory changes. The
default overlay factory explicitly uses viewcompose-overlay-android, never classpath discovery.
Destination context
LocalNavDestinationContext.current is non-null only while declaring content for the nearest
destination. Its stable NavDestinationContext exposes the exact NavEntry identity and a
read-only State<NavDestinationPresentation>. NavDestinationPresentation is a source alias for
the Navigation Core NavSceneEntry, so visibility, interaction, transition phase, pane role, and
content/overlay layer role cannot drift from the scene used for lifecycle planning.
fun UiTreeBuilder.destinationContextSample(controller: NavHostController) {
NavHost(controller = controller) { entry ->
val presentation = checkNotNull(LocalNavDestinationContext.current).presentation.value
Text("${entry.route.name}: ${presentation.visibility}, ${presentation.paneRole}")
}
}
Capture the context during DSL declaration for later callbacks. It survives presentation disposal for the retained entry and stops updating after permanent removal. AndroidX Lifecycle remains the resource threshold; presentation state is only for coarse visibility, pane, and transition UI. Nested hosts provide the nearest context, and there is no global current-page lookup.
Modal navigation scenes
Pass stable, ordered sceneStrategies to project trailing destinations above normal content panes;
the first strategy that returns a layout wins:
val ModalDestinationStrategy = NavSceneStrategies.trailingOverlays { entry ->
entry.route.name.endsWith("-dialog")
}
fun UiTreeBuilder.overlaySceneNavHostSample(controller: NavHostController) {
NavHost(
controller = controller,
sceneStrategies = listOf(ModalDestinationStrategy),
) { entry ->
Text(entry.route.name)
}
}
Covered layers remain visible at STARTED; only the top overlay owns input, accessibility, and
RESUMED. Its transparent full-host container blocks pointer, generic-motion, and key fallthrough;
destination content draws the surface and scrim. Overlays reuse content ownership, result, restore,
Back, and cleanup paths. Modal motion moves only the overlay and forbids cross-layer shared matching.
Layouts must retain content and classify an exact trailing stack suffix.
Return a result to the previous page
val SelectedItemResult = NavResultKey.text("catalog.selection")
fun UiTreeBuilder.observeSelectedItem(onSelected: (String) -> Unit) {
NavResultEffect(SelectedItemResult, onSelected)
}
fun returnSelectedItem(controller: NavHostController, itemId: String): NavResult =
controller.popBackStack(SelectedItemResult, itemId)
The committed pop enqueues into the surviving entry's saved FIFO inbox. NavResultEffect consumes
at most once after its destination is RESUMED; use NavDestinationContext.results for explicit
acknowledgement or retry. Keys are entry-local, not a global or cross-stack bus.
Presentation retention
NavPresentationRetentionPolicy controls native presentation lifetime independently of entry
ownership. DisposeWhenHidden is the default: after transition settlement, every fully hidden
child RenderSession and View tree is disposed while its entry owner remains at CREATED.
RetainAll is an explicit unbounded opt-in for surfaces whose measured rebuild cost justifies the
memory, effect, focus, accessibility, and native-resource cost. Bounded retains a positive maximum
of hidden presentations and evicts the least-recently-hidden presentation deterministically.
Visible scene entries and ordinary or predictive transition participants do not count against that bound.
fun UiTreeBuilder.BoundedPresentationNavigation(controller: NavHostController) {
NavHost(
controller = controller,
presentationRetentionPolicy = NavPresentationRetentionPolicy.Bounded(
maxHiddenPresentations = 2,
),
) { entry ->
Text(entry.route.name)
}
}
Changing the policy on an existing host does not recreate the host or any entry owner. Tightening
the bound disposes excess hidden presentations immediately. Relaxing it affects presentations that
are created or hidden later; it does not eagerly build pages that are not visible. On initial,
configuration-restored, or process-restored attachment, only the current scene layout is
materialized even under RetainAll.
Retention trade-offs and interpreted evidence are maintained by the navigation architecture.
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.
Typed plan execution
Navigation Core's reducer is the sole lifecycle, retention, input, accessibility, and Back policy
source. The Android executor prepares presentations before commit, then publishes the planned scene
and ordered effects; rollback and terminal cleanup consume plan IDs instead of inspecting Views.
Applications normally use NavHost; the reducer is a Q3 boundary for tests and custom executors.
Destination and graph ownership
Each destination entry owns independent Lifecycle, ViewModelStore, SavedStateRegistry,
SavedStateHandle defaults, and saveable state; graph instances own the same scope set for their
descendants. Hidden retention preserves those identities while capping Lifecycle. Transition
participants stay at most STARTED, exiting popped entries stay CREATED until presentation
disposal, and only permanent removal reaches DESTROYED. Duplicate routes still create distinct
owners.
NavHost requires the nearest LocalViewModelStoreOwner and inherits its default Factory and
CreationExtras; low-level renderInto callers provide it explicitly. A persisted host-scope ID
allows configuration recreation to lease the same entry and graph stores, while permanent removal
clears them. Use ProvideNavGraphOwner(route) within destination content to select an active graph
scope. Content panes and modal overlays use these same owners and lifecycle thresholds; the
separate UI overlay transport is only the mechanism used for transient motion snapshots.
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, scene layout, 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
The saveable registry persists stacks, history, entry/graph IDs and routes, owner bundles, and saveable values—not pending work, Views, sessions, Lifecycle objects, or ViewModel contents. Restored attachment materializes only the current content-and-overlay scene. Scene strategies are reapplied to the restored stack and current width. Invalid versions, shapes, limits, configurations, or graph hierarchies fail closed to initial state; the preceding version-4 format is accepted with a fresh host-scope identity.
Android system and predictive Back
While STARTED and able to pop, NavHost installs one handler on the nearest
ViewTreeNavigationEventDispatcherOwner, falling back to Activity Back only when that owner is
absent. A root disables the handler for outer fallback. Both mutually exclusive paths use one
transactional preview/pop state machine: cancellation restores the scene, commit pops once, and
stop, detach, disablement, owner change, or destruction cancels before unregistering and suppresses
the cancelled gesture's late terminal. Forward history and Android Studio Preview input are absent.
Motion
NavTransitionSpec is visual-only policy for every command and predictive Back.
NavDestinationTransform combines pane/dp travel, alpha, scale, timing, and easing; None disables
motion. The driver lays out endpoints before motion, uses temporary hardware layers for transform
work, and redirects from current visual properties without changing stack or owner semantics.
Shared content motion
sharedElement and sharedBounds are Q3 markers matched once per key and mode within one
destination pair. Invalid, detached, surface-backed, or over-budget endpoints fall back per key
without affecting navigation. The one-window implementation animates bounded snapshots in a
non-interactive overlay, preserves incoming input/accessibility ownership, cleans up exactly once,
and lets predictive Back drive the same visuals without gaining commit authority. Modal
transitions skip shared matching so snapshots cannot cross the overlay boundary.
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 content scene before recalculating native child bounds. Scene strategies run before pane selection, so overlay classification is stable across widths and only the content prefix is reflowed. Layout direction maps primary-to-tertiary roles to the correct physical order for LTR and RTL.
Deep links and retained stacks
fun navigateSharedImageRequest(controller: NavHostController): NavDeepLinkResult {
return controller.navigateDeepLink(
NavDeepLinkRequest(
action = Intent.ACTION_SEND,
mimeType = "image/png",
),
)
}
fun navigateSharedImageIntent(
controller: NavHostController,
intent: Intent,
): NavDeepLinkResult {
return controller.navigateDeepLink(intent)
}
Request, URI, and Intent entry points share the strict Core resolver; Intent maps only data,
action, and type. A match atomically updates and selects its target stack, while the nested
NavResult preserves render/commit failure. Multiple tabs use one remembered controller and
NavStackConfiguration, without mirrored active-stack state.
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.
The Lifecycle 2.11 hard cut requires NavHost to run below a LocalViewModelStoreOwner. Existing
Activity and Fragment setUiContent integrations satisfy the requirement. A custom renderInto
host must add ProvideViewModelStoreOwner; no implicit root store or compatibility alias is kept.
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.
This Alpha inserts sceneStrategies before presentationRetentionPolicy; positional calls at that
point must migrate to named arguments. Keep strategy instances stable across recomposition.