Navigation 3
Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)
- Skill
- Tool integration
- Software development
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides developers with the necessary documentation, migration steps, and code recipes to adopt Jetpack Navigation 3. It covers the core concepts of the library, including back stack management, adaptive layouts using Scenes, deep link handling, and integration with Hilt, ViewModel, and Material 3 Adaptive components. The resource is designed to help developers transition from Navigation 2 or implement new navigation flows with a focus on type-safe, Compose-first architecture.
What it does
- Provides a step-by-step migration path from Navigation 2 to Navigation 3.
- Explains how to define destinations, manage back stacks, and use NavDisplay.
- Offers code recipes for common patterns like bottom sheets, dialogs, and adaptive layouts.
- Demonstrates how to handle deep links, animations, and conditional navigation.
- Shows integration with dependency injection frameworks like Hilt and Koin.
- Details how to pass arguments to ViewModels and return results between screens.
When to use it
- When you need to migrate an existing Android app from Navigation 2 to Navigation 3.
- When building a new Android application using Jetpack Compose.
- When implementing complex navigation patterns like multiple back stacks or adaptive list-detail views.
- When you need to handle deep links or conditional navigation flows in a type-safe manner.
Advantages
- Full control over the back stack and navigation state.
- Native support for adaptive layouts and multi-pane displays.
- Strongly typed navigation routes.
- Simplified integration with Jetpack Compose and ViewModel.
- Extensive library of code recipes for common UI and architectural patterns.
Usage examples
- “How do I migrate my existing Android app from Navigation 2 to Navigation 3?”
- The agent will guide you through the migration steps, including updating dependencies, replacing NavController with the new state management classes, and switching from NavHost to NavDisplay.
- “Show me how to implement a list-detail layout using Navigation 3.”
- The agent will provide the relevant code recipe for implementing adaptive list-detail scenes using the Navigation 3 Scenes API.
- “How can I handle conditional navigation for an authentication flow?”
- The agent will explain how to switch navigation flows based on conditions, such as checking user login status, using the conditional navigation recipe.
- “How do I pass arguments to a ViewModel in Navigation 3?”
- The agent will demonstrate how to pass navigation arguments to a ViewModel constructed using the viewModel() function.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/android/navigation-3/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip navigation-3.zip -d .agents/skills
Documentation by Google LLC
Migration guide
Navigation 2 to Navigation 3 migration guide: Step-by-step guide to migrate an Android application from Navigation 2 to Navigation 3, covering dependency updates, route changes, state management, and UI component replacements.
Requirements
Guide: Migrate to type-safe navigation in Compose : Step-by-step guide to migrating an Android application from string-based navigation to Type-Safe Navigation in Jetpack Compose using Jetpack Navigation 2.
Developer documentation
Navigation 3. Search documentation for more information on basics, saving and managing navigation state, modularizing navigation code, creating custom layouts using Scenes, animating between destinations, or applying logic or wrappers to destinations.
Recipes
Code examples showcasing common patterns.
Basic API usage
Basic: Shows most basic API usage.
Saveable back stack: Shows basic API usage with a persistent back stack.
Entry provider DSL: Shows basic API usage using the entryProvider DSL.
Common UI
Common UI: Demonstrates how to implement a common navigation UI pattern with a bottom navigation bar and multiple back stacks, where each tab in the navigation bar has its own navigation history.
Deep links
Static URI: Shows how to handle simple static URI deep links.
URI with Arguments: Shows how to parse path and query arguments from a deep link.
Synthetic BackStack: Shows how to handle deep links with a synthetic back stack.
Custom Matcher: Shows how to implement custom deep link matching logic.
Scenes
Use built-in Scenes
Dialog: Shows how to create a Dialog.
Create custom Scenes
BottomSheet: Shows how to create a BottomSheet destination.
List-Detail Scene: Demonstrates how to implement adaptive list-detail layouts using the Navigation 3 Scenes API.
Two pane Scene: Demonstrates how to implement adaptive two-pane layouts using the Navigation 3 Scenes API.
Material Adaptive
Material List-Detail: Demonstrates how to implement an adaptive list-detail layout using Material 3 Adaptive.
Material Supporting Pane: Demonstrates how to implement an adaptive supporting pane layout using Material 3 Adaptive.
Animations
Animations: Shows how to override the default animations for all destinations and a single destination.
Conditional Transitions: Shows how to implement conditional transition animations.
Common back stack behavior
Multiple back stacks: Shows how to create multiple top level routes, each with its own back stack. Top level routes are displayed in a navigation bar allowing users to switch between them. State is retained for each top level route, and the navigation state persists config changes and process death.
Conditional navigation
Conditional navigation: Switch to a different navigation flow when a condition is met. For example, for authentication or first-time user onboarding.
Lifecycle
Lifecycle Owner: Shows how to use and observe Lifecycle in Navigation 3.
Architecture
Modularized navigation code (Hilt): Demonstrates how to decouple navigation code into separate modules using Hilt or Dagger for DI.
Modularized navigation code (Koin): Demonstrates how to decouple navigation code into separate modules using Koin for DI.
Working with ViewModel
Passing navigation arguments
Basic ViewModel : Navigation arguments are passed to a
ViewModelconstructed usingviewModel()
Returning results
Returning Results as Events : Returning results as events to content in another
NavEntryReturning Results as State : Returning results as state stored in a
CompositionLocal
Files
- ATTRIBUTION.mdAdded by AutoGineer955 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md6.5 KiB
- references/android/guide/navigation/navigation-3/index.md3.5 KiB
- references/android/guide/navigation/navigation-3/migration-guide.md26.5 KiB
- references/android/guide/navigation/navigation-3/recipes/animations.md6.4 KiB
- references/android/guide/navigation/navigation-3/recipes/basic.md3.5 KiB
- references/android/guide/navigation/navigation-3/recipes/basicdsl.md3.5 KiB
- references/android/guide/navigation/navigation-3/recipes/basicsaveable.md3.5 KiB
- references/android/guide/navigation/navigation-3/recipes/bottomsheet.md7.8 KiB
- references/android/guide/navigation/navigation-3/recipes/common-ui.md8.5 KiB
- references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md8.4 KiB
- references/android/guide/navigation/navigation-3/recipes/conditional.md9.4 KiB
- references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md7.1 KiB
- references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md5.5 KiB
- references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md7.7 KiB
- references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md15.4 KiB
- references/android/guide/navigation/navigation-3/recipes/dialog.md4.4 KiB
- references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md8.2 KiB
- references/android/guide/navigation/navigation-3/recipes/material-listdetail.md6.4 KiB
- references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md6.6 KiB
- references/android/guide/navigation/navigation-3/recipes/modular-hilt.md12.4 KiB
- references/android/guide/navigation/navigation-3/recipes/modular-koin.md13.3 KiB
- references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md16.6 KiB
- references/android/guide/navigation/navigation-3/recipes/passingarguments.md16.0 KiB
- references/android/guide/navigation/navigation-3/recipes/results-event.md9.8 KiB
- references/android/guide/navigation/navigation-3/recipes/results-state.md9.6 KiB
- references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md15.9 KiB
- references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md9.1 KiB
- references/android/guide/navigation/type-safe-destinations.md4.1 KiB
references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md
Multiple back stacks recipe
This recipe demonstrates how to create multiple back stacks.
The app has three top level routes: RouteA, RouteB and RouteC. These routes have sub routes RouteA1, RouteB1 and RouteC1 respectively. RouteA1 contains a 100-item scrollable list, while RouteB1 and RouteC1 contain counters used to verify state retention through configuration changes and process death.
The app's navigation state is held in the NavigationState class. The state itself is created using rememberNavigationState.
Navigation events are handled by the Navigator. It updates the navigation state.
The navigation state is converted into NavEntrys with NavigationState.toDecoratedEntries. These entries are then displayed by NavDisplay.
Key behaviors:
This app follows the "exit through home" pattern where the user always exits through the starting back stack. This means that
RouteA's entries are always in the list of entries.Navigating to a top level route that is not the starting route replaces the other entries. For example, navigating A->B->C would result in entries for A+C, B's entries are removed.
When a top-level route is reselected, for example if the user is on
RouteAand tapsRouteAon the navigation bar again, theNavigationBarsignals the reselected key toNavigator, which emits it to aFlow<NavKey>.RouteA1observes this flow and, when it receivesRouteA, resets its list scroll position to index 0.
Important implementation details:
Each top level route has its own
SaveableStateHolderNavEntryDecorator. This is the object responsible for managing the state for the entries in its back stack.
Image: untitled Explore View the full recipe on GitHub.
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.nav3recipes.multiplestacks
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
/**
* Handles navigation events (forward and back) by updating the navigation state.
*/
class Navigator(val state: NavigationState){
private val _reselectEvents = MutableSharedFlow<NavKey>(extraBufferCapacity = 1)
val reselectEvents = _reselectEvents.asSharedFlow()
fun navigate(route: NavKey){
if (route in state.backStacks.keys){
// This is a top level route, just switch to it
state.topLevelRoute = route
} else {
state.backStacks[state.topLevelRoute]?.add(route)
}
}
fun onReselect(route: NavKey) {
_reselectEvents.tryEmit(route)
}
fun goBack(){
val currentStack = state.backStacks[state.topLevelRoute] ?:
error("Stack for ${state.topLevelRoute} not found")
val currentRoute = currentStack.last()
// If we're at the base of the current route, go back to the start route stack.
if (currentRoute == state.topLevelRoute){
state.topLevelRoute = state.startRoute
} else {
currentStack.removeLastOrNull()
}
}
}/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.nav3recipes.multiplestacks
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
/**
* Create a navigation state that persists config changes and process death.
*
* @param startRoute - The top level route to start on. This should also be in `topLevelRoutes`.
* @param topLevelRoutes - The top level routes in the app.
*/
@Composable
fun rememberNavigationState(
startRoute: NavKey,
topLevelRoutes: Set<NavKey>
): NavigationState {
val topLevelRoute = rememberNavKey(startRoute)
// Create a back stack for each top level route.
val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }
return remember(startRoute, topLevelRoutes) {
NavigationState(
startRoute = startRoute,
topLevelRoute = topLevelRoute,
backStacks = backStacks
)
}
}
/**
* State holder for navigation state. This class does not modify its own state. It is designed
* to be modified using the `Navigator` class.
*
* @param startRoute - the start route. The user will exit the app through this route.
* @param topLevelRoute - the state object that backs the top level route.
* @param backStacks - the back stacks for each top level route.
*/
class NavigationState(
val startRoute: NavKey,
topLevelRoute: MutableState<NavKey>,
val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
/**
* The top level route.
*/
var topLevelRoute: NavKey by topLevelRoute
/**
* Convert the navigation state into `NavEntry`s that have been decorated with a
* `SaveableStateHolder`.
*
* @param entryProvider - the entry provider used to convert the keys in the
* back stacks to `NavEntry`s.
*/
@Composable
fun toDecoratedEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>
): List<NavEntry<NavKey>> {
// For each back stack, create a `SaveableStateHolder` decorator and use it to decorate
// the entries from that stack. When backStacks changes, `rememberDecoratedNavEntries` will
// be recomposed and a new list of decorated entries is returned.
val decoratedEntries = backStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
rememberViewModelStoreNavEntryDecorator<NavKey>()
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider
)
}
// Only return the entries for the stacks that are currently in use.
return getTopLevelRoutesInUse()
.flatMap { decoratedEntries[it] ?: emptyList() }
}
/**
* Get the top level routes that are currently in use. The start route is always the first route
* in the list. This means the user will always exit the app through the starting route
* ("exit through home" pattern). The list will contain a maximum of one other route. This is a
* design decision. In your app, you may wish to allow more than two top level routes to be
* active.
*
* Note that even if a top level route is not in use its state is still retained.
*
* @return the current top level routes that are in use.
*/
private fun getTopLevelRoutesInUse() : List<NavKey> =
if (topLevelRoute == startRoute) {
listOf(startRoute)
} else {
listOf(startRoute, topLevelRoute)
}
}/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.nav3recipes.multiplestacks
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.serialization.Serializable
@Serializable
data object RouteA : NavKey
@Serializable
data object RouteA1 : NavKey
@Serializable
data object RouteB : NavKey
@Serializable
data object RouteB1 : NavKey
@Serializable
data object RouteC : NavKey
@Serializable
data object RouteC1 : NavKey
private val TOP_LEVEL_ROUTES = mapOf<NavKey, NavBarItem>(
RouteA to NavBarItem(icon = Icons.Default.Home, description = "Route A"),
RouteB to NavBarItem(icon = Icons.Default.Face, description = "Route B"),
RouteC to NavBarItem(icon = Icons.Default.Camera, description = "Route C"),
)
data class NavBarItem(
val icon: ImageVector,
val description: String
)
class MultipleStacksActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
setContent {
val navigationState = rememberNavigationState(
startRoute = RouteA,
topLevelRoutes = TOP_LEVEL_ROUTES.keys
)
val navigator = remember { Navigator(navigationState) }
val entryProvider = entryProvider {
featureASection(
reselectEvents = navigator.reselectEvents,
onSubRouteClick = { navigator.navigate(RouteA1) }
)
featureBSection(onSubRouteClick = { navigator.navigate(RouteB1) })
featureCSection(onSubRouteClick = { navigator.navigate(RouteC1) })
}
Scaffold(contentWindowInsets = WindowInsets(0.dp), bottomBar = {
NavigationBar {
TOP_LEVEL_ROUTES.forEach { (key, value) ->
val isSelected = key == navigationState.topLevelRoute
NavigationBarItem(
selected = isSelected,
onClick = {
navigator.navigate(key)
if (isSelected) {
navigator.onReselect(key)
}
},
icon = {
Icon(
imageVector = value.icon,
contentDescription = value.description
)
},
label = { Text(value.description) }
)
}
}
}) { innerPadding ->
NavDisplay(
entries = navigationState.toDecoratedEntries(entryProvider),
onBack = { navigator.goBack() },
modifier = Modifier.padding(innerPadding)
)
}
}
}
}/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.nav3recipes.multiplestacks
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import com.example.nav3recipes.content.ContentGreen
import com.example.nav3recipes.content.ContentMauve
import com.example.nav3recipes.content.ContentOrange
import com.example.nav3recipes.content.ContentPink
import com.example.nav3recipes.content.ContentPurple
import com.example.nav3recipes.content.ContentRed
import kotlinx.coroutines.flow.Flow
fun EntryProviderScope<NavKey>.featureASection(
reselectEvents: Flow<NavKey>,
onSubRouteClick: () -> Unit,
) {
entry<RouteA> {
ContentRed("Route A") {
Button(onClick = dropUnlessResumed(block = onSubRouteClick)) {
Text("Go to A1")
}
}
}
entry<RouteA1> {
val scrollState = rememberLazyListState()
LaunchedEffect(reselectEvents) {
reselectEvents.collect { route ->
if (route == RouteA) {
scrollState.scrollToItem(0)
}
}
}
ContentPink("Route A1") {
LazyColumn(state = scrollState) {
items(100) { index ->
Text("Route A item ${index + 1}", fontSize = 24.sp)
}
}
}
}
}
fun EntryProviderScope<NavKey>.featureBSection(
onSubRouteClick: () -> Unit,
) {
entry<RouteB> {
ContentGreen("Route B") {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = dropUnlessResumed(block = onSubRouteClick)) {
Text("Go to B1")
}
}
}
}
entry<RouteB1> {
ContentPurple("Route B1") {
var count by rememberSaveable {
mutableIntStateOf(0)
}
Button(onClick = { count++ }) {
Text("Value: $count")
}
}
}
}
fun EntryProviderScope<NavKey>.featureCSection(
onSubRouteClick: () -> Unit,
) {
entry<RouteC> {
ContentMauve("Route C") {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = dropUnlessResumed(block = onSubRouteClick)) {
Text("Go to C1")
}
}
}
}
entry<RouteC1> {
ContentOrange("Route C1") {
var count by rememberSaveable {
mutableIntStateOf(0)
}
Button(onClick = { count++ }) {
Text("Value: $count")
}
}
}
}Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Google LLC
- Publisher
- Android (Google) (Official)
- Distributed by
- Android (Google)
- Source
- android/skills · navigation/navigation-3 · commit bf68e2d
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE.txt)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Google LLC or Android (Google).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download navigation-3.zip