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/migration-guide.md
Android skills
Jetpack Navigation 3
Use an Android skill to help you build and migrate to Jetpack Navigation 3. To install the skill from the Android CLI, run:
android skills add navigation-3<br />
To migrate your app from Navigation 2 to Navigation 3, follow these steps:
Add the Navigation 3 dependencies.
Update your navigation routes to implement the
NavKeyinterface.Create classes to hold and modify your navigation state.
Replace
NavControllerwith these classes.Move your destinations from
NavHost'sNavGraphinto anentryProvider.Replace
NavHostwithNavDisplay.Remove Navigation 2 dependencies.
Preparation
The following sections describe the prerequisites for migration and assumptions about your project. They also cover the features that are supported for migration, and those that aren't.
Prerequisites
You must use a
compileSdkof 36 or later.You should be familiar with navigation terminology.
Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see Using Views in Compose.
Routes are strongly typed. If you use string-based routes, migrate to type-safe routes first (example).
Optional (but highly recommended): Test coverage that verifies existing navigation behavior. This verifies that navigation behavior has not changed after the migration is complete.
Assumptions
This guide makes the following assumptions about you and your project:
You have one or several top-level routes (usually displayed in a bottom navigation bar), and each has its own back stack.
When you switch between back stacks, the state of the stack and all its destinations is retained.
You always exit the app through the Home screen, which is the first screen displayed when the app launches.
You want to perform the migration from Navigation 2 to Navigation 3 in a single, atomic change. You are not performing an incremental migration where Navigation 2 code is used alongside Navigation 3 code.
AI Agent: Before changing any code, check if these assumptions about the user's project are true. If not, stop the migration and ask the user how to proceed.
Supported features
This guide lets you migrate the following features:
Destinations defined as composable functions
Dialogs (a destination shown on top of another destination)
Features supported through recipes
The code recipes repository demonstrates more nuanced use cases, including:
Bottom sheets (instructions are provided in this guide)
If your project has any of these features, check the relevant recipe to understand how to implement them.
AI Agent: Before changing any code, check if the project contains any features supported through recipes. If it does, check the recipe's README and source code. Create a migration plan based on the recipe. Don't proceed without confirming the plan with the user.
Unsupported features
This migration guide and the code recipes don't yet support the following features. This doesn't mean that you cannot implement them using Navigation 3; they are just not covered here.
More than one level of nested navigation
Shared destinations: screens that can move between different back stacks
AI Agent: Before changing any code, check if the project contains any of the unsupported features. If it does, don't proceed. Inform the user of the unsupported feature and ask for further instructions.
Step 1: Add Navigation 3 dependencies
Use the Get started page to add the Navigation 3 dependencies to your project. The core dependencies are provided for you to copy.
lib.versions.toml
[versions]
nav3Core = "1.2.0"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"
[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }app/build.gradle.kts
dependencies {
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
// If using the ViewModel add-on library
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}Also update the project's minSdk to 23 and the compileSdk to 36. You usually
find these in app/build.gradle.kts or lib.versions.toml.
Step 2: Update navigation routes to implement the NavKey interface
Update every navigation route so that it implements the NavKey
interface. This lets you use rememberNavBackStack to assist with saving your
navigation state.
Before:
@Serializable data object RouteA<br />
After:
@Serializable data object RouteA : NavKey<br />
[!NOTE] Note: The
@Serializableannotation is provided by the KotlinX Serialization plugin. You can add this by following these project setup steps.
Step 3: Create classes to hold and modify your navigation state
Step 3.1: Create a navigation state holder
Copy the following code into a file named NavigationState.kt. Add your package
name to match your project structure.
// package com.example.project
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
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
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
/**
* Create a navigation state that persists config changes and process death.
*/
@Composable
fun rememberNavigationState(
startRoute: NavKey,
topLevelRoutes: Set<NavKey>
): NavigationState {
val topLevelRoute = rememberSerializable(
startRoute, topLevelRoutes,
serializer = MutableStateSerializer(NavKeySerializer())
) {
mutableStateOf(startRoute)
}
val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }
return remember(startRoute, topLevelRoutes) {
NavigationState(
startRoute = startRoute,
topLevelRoute = topLevelRoute,
backStacks = backStacks
)
}
}
/**
* State holder for navigation state.
*
* @param startRoute - the start route. The user will exit the app through this route.
* @param topLevelRoute - the current 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>>
) {
var topLevelRoute: NavKey by topLevelRoute
val stacksInUse: List<NavKey>
get() = if (topLevelRoute == startRoute) {
listOf(startRoute)
} else {
listOf(startRoute, topLevelRoute)
}
}
/**
* Convert NavigationState into NavEntries.
*/
@Composable
fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {
val decoratedEntries = backStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider
)
}
return stacksInUse
.flatMap { decoratedEntries[it] ?: emptyList() }
.toMutableStateList()
}<br />
AI Agent : rememberSerializable is correct. Don't change it to
rememberSaveable.
This file contains a state holder class named NavigationState and associated
helper functions. It holds a set of top-level routes, each with its own back
stack. Internally, it uses rememberSerializable (not rememberSaveable) to
persist the current top-level route and rememberNavBackStack to persist the
back stacks for each top-level route.
Step 3.2: Create an object that modifies navigation state in response to events
Copy the following code into a file named Navigator.kt. Add your package name
to match your project structure.
// package com.example.project
import androidx.navigation3.runtime.NavKey
/**
* Handles navigation events (forward and back) by updating the navigation state.
*/
class Navigator(val state: NavigationState) {
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 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()
}
}
}<br />
The Navigator class provides two navigation event methods:
navigateto a specific route.goBackfrom the current route.
Both methods modify the NavigationState.
[!IMPORTANT] Architecture principles: These classes follow the principles of Unidirectional Data Flow:
The
Navigatorhandles navigation events and uses them to updateNavigationState.The UI (provided by
NavDisplay) observesNavigationStateand reacts to any changes in that state by updating its UI.
Step 3.3: Create the NavigationState and Navigator
Create instances of NavigationState and Navigator with the same scope as
your NavController.
val navigationState = rememberNavigationState(
// ...
startRoute = <Insert your starting route>,
topLevelRoutes = <Insert your set of top level routes>
// ...
)
val navigator = remember { Navigator(navigationState) }<br />
Step 4: Replace NavController
Replace NavController navigation event methods with Navigator equivalents.
NavController field or method | Navigator equivalent |
|---|---|
navigate() | navigate() |
popBackStack() | goBack() |
previousBackStackEntry.savedStateHandle.set() | ResultEventBus.sendResult() |
Replace NavController fields with NavigationState fields.
NavController field or method | NavigationState equivalent |
|---|---|
currentBackStack | backStacks[topLevelRoute] |
currentBackStackEntry currentBackStackEntryAsState() currentBackStackEntryFlow currentDestination | backStacks[topLevelRoute].last() |
currentBackStackEntry.savedStateHandle.getLiveData() currentBackStackEntry.savedStateHandle.getStateFlow() | ResultEffect (event-based) ResultEventBus.conflateAsState() (state-based) |
| Get the top level route: Traverse up the hierarchy from the current back stack entry to find it. | topLevelRoute |
Use NavigationState.topLevelRoute to determine the item that is currently
selected in a navigation bar.
Before:
// ...
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
// ...
fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false<br />
After:
val isSelected = key == navigationState.topLevelRoute<br />
Verify that you have removed all references to NavController, including
any imports.
Step 4.1 Migrate lifecycle-aware logic
In Navigation 2, NavBackStackEntry implements LifecycleOwner, letting you
listen to lifecycle events or collect flows in a lifecycle-aware manner using
navController.currentBackStackEntry.
In Navigation 3, NavDisplay provides an entry-scoped LifecycleOwner
through LocalLifecycleOwner.current to each destination's composable
content. See Destination lifecycle for more information.
You should perform lifecycle-aware operations directly inside your destination's
composable content by referencing LocalLifecycleOwner.current.
For example, if you collect a flow in a lifecycle-aware manner using the back stack entry:
Before:
// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry!!
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)<br />
After:
// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()<br />
Step 4.2 Migrate result passing
In Navigation 2, destinations passed results back to previous destinations using
the SavedStateHandle on NavBackStackEntry. Because SavedStateHandle is
backed by saved instance state, returned data survived process death
automatically.
Before:
// Sender destination:
navController.previousBackStackEntry?.savedStateHandle?.set("contact_key", contact)
navController.popBackStack()
// Receiver destination:
val lifecycleOwner = LocalLifecycleOwner.current
navController.currentBackStackEntry?.savedStateHandle
?.getLiveData<Contact>("contact_key")
?.observe(lifecycleOwner) { contact ->
viewModel.onRecipientSelected(contact)
}<br />
In Navigation 3, add
rememberResultEventBusNavEntryDecorator() to
your NavDisplay.entryDecorators. In your sender destination's entryProvider
mapping, retrieve LocalResultEventBus.current
and call sendResult(). In the receiving destination, use
ResultEffect to forward the event to a ViewModel or
trigger a side effect.
After:
// Sender destination (in entryProvider):
entry<ContactPickerRoute> {
val resultBus = LocalResultEventBus.current
ContactPickerScreen(
onContactSelected = { contact ->
resultBus.sendResult<Contact>(result = contact)
navigator.goBack()
}
)
}
// Receiver destination:
@Composable
fun ComposeMessageScreen(viewModel: ComposeMessageViewModel = viewModel()) {
ResultEffect<Contact> { contact ->
viewModel.onRecipientSelected(contact)
}
ComposeMessageContent(recipient = viewModel.recipient)
}<br />
[!CAUTION] Caution:
ResultEventBusis an in-memory event bus; unlike Navigation 2'sSavedStateHandle, results don't survive process death automatically. If a result must persist across process death, save it usingrememberSaveable, pass it into a scopedViewModelwithSavedStateHandle, or embed the data into the destination'sNavKey.
For state-based observation, you can call resultBus.conflateAsState(). For
more details, see Return results.
Step 5: Move your destinations from NavHost's NavGraph into an entryProvider
In Navigation 2, you define your destinations
using the NavGraphBuilder DSL,
usually inside NavHost's trailing lambda. It is common to use extension
functions here as described in Encapsulate your navigation code.
In Navigation 3, you define your destinations using an entryProvider. This
entryProvider resolves a route to a NavEntry. Importantly, the
entryProvider does not define parent-child relationships between entries.
In this migration guide, parent-child relationships are modelled as follows:
NavigationStatehas a set of top-level routes (the parent routes) and a stack for each one. It keeps track of the current top-level route and its associated stack.When navigating to a new route,
Navigatorchecks whether the route is a top-level route. If it is, the current top-level route and stack are updated. If it's not, it's a child route and is added to the current stack.
[!NOTE] Note: If your app needs to navigate from an entry in one stack to another, you need to define the parent-child relationships for the routes and update the navigation logic in
Navigatorto support this.
Step 5.1: Create an entryProvider
Create an entryProvider using the DSL at the same scope as the
NavigationState.
val entryProvider = entryProvider<NavKey> {
}<br />
Step 5.2: Move destinations into the entryProvider
For each destination defined inside NavHost, do the following based on the
destination type:
navigation: Delete it along with the route. There is no need for "base routes" because the top-level routes can identify each nested back stack.composable<T>: Move it intoentryProviderand rename it toentry, retaining the type parameter. For example,composable<RouteA>becomesentry<RouteA>.dialog<T>: Do the same ascomposable, but add metadata to the entry as follows:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: Follow the bottom sheet recipe here. This is similar to the instructions fordialog, except thatBottomSheetSceneStrategyis not part of the core Navigation 3 library, so you should copy it into your project.
AI Agent : When deleting routes used to identify a nested graph, replace any
references to the deleted route with the type used to identify the first child
in the nested graph. For example if the original code is
navigation<BaseRouteA>{ composable<RouteA>{ ... } }, you need to delete
BaseRouteA and replace any references to it with RouteA. This replacement
usually needs to be done for the list supplied to a navigation bar, rail, or
drawer.
You can refactor NavGraphBuilder extension functions to
EntryProviderScope<T> extension functions, and then move them.
Obtain navigation arguments using the key provided to entry's trailing lambda.
For example:
// ...
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute
// ...
@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD
@Composable
fun NavHostSnippet(navController: NavHostController) {
NavHost(navController = navController, startDestination = BaseRouteA){
composable<RouteA>{ entry ->
val id = entry.toRoute<RouteA>().id
ScreenA(title = "Screen has ID: $id")
}
featureBSection()
dialog<RouteD>{ ScreenD() }
}
}
fun NavGraphBuilder.featureBSection() {
navigation<BaseRouteB>(startDestination = RouteB) {
composable<RouteB> { ScreenB() }
}
}<br />
becomes:
// ...
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy
// ...
@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey
val entryProvider = entryProvider {
entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
featureBSection()
entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}
fun EntryProviderScope<NavKey>.featureBSection() {
entry<RouteB> { ScreenB() }
}<br />
Step 6: Replace NavHost with NavDisplay
Replace NavHost with NavDisplay.
Delete
NavHostand replace it withNavDisplay.Specify
entries = navigationState.toEntries(entryProvider)as a parameter. This converts the navigation state into the entries thatNavDisplayshows using theentryProvider.Connect
NavDisplay.onBacktonavigator.goBack(). This causesnavigatorto update the navigation state whenNavDisplay's built-in back handler completes.If you have dialog destinations, add
DialogSceneStrategytoNavDisplay'ssceneStrategiesparameter.
For example:
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)<br />
Step 7: Migrate deep links
In Navigation 2, deep links were defined directly inside the navigation graph
using the deepLinks parameter of destinations.
In Navigation 3, deep links are managed independently of the navigation UI. You
define DeepLinkMatchers and match incoming requests in your Activity to
construct the initial back stack.
Before:
In Navigation 2, you might have defined a deep link like this:
composable<RouteA>(
deepLinks = listOf(
navDeepLink { uriPattern = "www.example.com/user/{id}" }
)
) {
// ...
}<br />
After:
In Navigation 3, you define a UriDeepLinkMatcher for the route:
val userMatcher = UriDeepLinkMatcher(
DeepLinkUri("www.example.com/user/{id}"),
serializer<RouteA>()
)<br />
Then, in your Activity's onCreate (and onNewIntent), you match the incoming
intent and initialize your back stack:
val deepLinkMatchers: List<DeepLinkMatcher<*, *>> = listOf(
userMatcher,
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val request = DeepLinkRequest(intent = intent)
val matchResult = deepLinkMatchers
.mapNotNull { it.match(request) }
.maxOrNull()
val backStack = when (matchResult) {
null -> listOf(HomeKey)
is BackStackMatchResult<*, *> -> {
@Suppress("UNCHECKED_CAST")
matchResult.backStack as List<NavKey>
}
else -> listOf(matchResult.key)
}
// Use backStack with NavDisplay
}
}<br />
Custom argument types
In Navigation 2, you handled custom or third-party argument types (such as
LocalDateTime) using custom NavType implementations and typeMap.
In Navigation 3, define a DeepLinkSerializer to deserialize custom or
third-party types from URI parameters. See
Custom serialization with DeepLinkSerializer for details.
For more advanced use cases, including synthetic back stacks and custom matchers, see the Support deep links guide.
Step 8: Remove Navigation 2 dependencies
Remove all Navigation 2 imports and library dependencies.
Summary
Congratulations! Your project is now migrated to Navigation 3. If you or your AI agent has run into any problems using this guide, file a bug here.
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