Generated banner for Navigation 3
Generated logo for Navigation 3

Navigation 3

Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)

  • Skill
  • Tool integration
  • Software development
  • Apache-2.0
  • No issues found

From the author

Learn how to install and migrate to Jetpack Navigation 3, and how to implement features and patterns such as deep links, multiple backstacks, scenes (dialogs, bottom sheets, list-detail, two-pane, supporting pane), conditional navigation (such as logged-in navigation versus anonymous), returning results from flows, integration with Hilt, ViewModel, Kotlin, and view interoperability.
— the description by Google LLC

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/android/navigation-3/download
  2. Unzip 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

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
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.

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
Architecture
Working with ViewModel
Passing navigation arguments
  • Basic ViewModel : Navigation arguments are passed to a ViewModel constructed using viewModel()

Returning results

Files

references/android/guide/navigation/navigation-3/migration-guide.md

Android skills

View on GitHub

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:

  1. Add the Navigation 3 dependencies.

  2. Update your navigation routes to implement the NavKey interface.

  3. Create classes to hold and modify your navigation state.

  4. Replace NavController with these classes.

  5. Move your destinations from NavHost's NavGraph into an entryProvider.

  6. Replace NavHost with NavDisplay.

  7. 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 compileSdk of 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:

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

  • Custom destination types

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 @Serializable annotation 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:

  • navigate to a specific route.

  • goBack from the current route.

Both methods modify the NavigationState.

[!IMPORTANT] Architecture principles: These classes follow the principles of Unidirectional Data Flow:

  • The Navigator handles navigation events and uses them to update NavigationState.

  • The UI (provided by NavDisplay) observes NavigationState and 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 methodNavigator equivalent
navigate()navigate()
popBackStack()goBack()
previousBackStackEntry.savedStateHandle.set()ResultEventBus.sendResult()

Replace NavController fields with NavigationState fields.

NavController field or methodNavigationState equivalent
currentBackStackbackStacks[topLevelRoute]
currentBackStackEntry currentBackStackEntryAsState() currentBackStackEntryFlow currentDestinationbackStacks[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: ResultEventBus is an in-memory event bus; unlike Navigation 2's SavedStateHandle, results don't survive process death automatically. If a result must persist across process death, save it using rememberSaveable, pass it into a scoped ViewModel with SavedStateHandle, or embed the data into the destination's NavKey.

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:

  • NavigationState has 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, Navigator checks 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 Navigator to 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 into entryProvider and rename it to entry, retaining the type parameter. For example, composable<RouteA> becomes entry<RouteA>.

  • dialog<T>: Do the same as composable, 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 for dialog, except that BottomSheetSceneStrategy is 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 NavHost and replace it with NavDisplay.

  • Specify entries = navigationState.toEntries(entryProvider) as a parameter. This converts the navigation state into the entries that NavDisplay shows using the entryProvider.

  • Connect NavDisplay.onBack to navigator.goBack(). This causes navigator to update the navigation state when NavDisplay's built-in back handler completes.

  • If you have dialog destinations, add DialogSceneStrategy to NavDisplay's sceneStrategies parameter.

For example:

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)

<br />

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