从 Navigation 2 迁移到 Navigation 3

如需将应用从 Navigation 2 迁移到 Navigation 3,请按以下步骤操作:

  1. 添加 Navigation 3 依赖项。
  2. 更新导航路线以实现 NavKey 接口。
  3. 创建用于保存和修改导航状态的类。
  4. NavController 替换为这些类。
  5. 将目的地从 NavHostNavGraph 移至 entryProvider
  6. NavHost 替换为 NavDisplay
  7. 移除 Navigation 2 依赖项。

使用 AI 智能体

您可以搭配 AI 智能体(例如 Android Studio 的智能体模式下的 Gemini)使用本指南。本指南中以“AI Agent:”开头的行应由 AI 代理读取,但人类读者可以忽略。

准备

以下部分介绍了迁移的前提条件以及有关项目的假设。这些指南还介绍了支持迁移的功能和不支持迁移的功能。

前提条件

  • 您必须使用 36 或更高版本的 compileSdk
  • 您应熟悉导航术语
  • 目的地是可组合函数。Navigation 3 专为 Compose 设计。如需在 Compose 中使用 Fragment 和 View,请参阅在 Compose 中使用 View
  • 路由经过强类型化。如果您使用的是基于字符串的路由,请先迁移到类型安全的路由示例)。
  • 可选(但强烈建议):用于验证现有导航行为的测试覆盖率。此测试可验证迁移完成后导航行为是否未发生变化。

假设

本指南对您和您的项目做出了以下假设:

  • 您有多个顶级路线(通常显示在底部导航栏中),每条路线都有自己的返回堆栈。
  • 当您在返回堆栈之间切换时,堆栈及其所有目的地的状态都会保留。
  • 您始终通过屏幕退出应用,该屏幕是应用启动时显示的第一个屏幕。
  • 您希望以单个原子性更改的形式执行从 Navigation 2 到 Navigation 3 的迁移。您不是在执行增量迁移,即同时使用 Navigation 2 代码和 Navigation 3 代码。

AI Agent:在更改任何代码之前,请检查有关用户项目的这些假设是否成立。如果不是,请停止迁移,并询问用户如何继续。

支持的功能

借助本指南,您可以迁移以下功能:

  • 定义为可组合函数的目的地
  • 对话框(显示在另一个目的地之上的目的地)

通过配方支持的功能

代码食谱代码库展示了更多细致的用例,包括:

如果您的项目包含上述任何功能,请查看相关方案,了解如何实现这些功能。

AI Agent:在更改任何代码之前,请检查项目是否包含通过配方支持的任何功能。如果符合,请查看配方的 README 和源代码。根据方案创建迁移计划。在未向用户确认方案之前,请勿继续操作。

不支持的功能

此迁移指南和代码示例尚不支持以下功能。这并不意味着您无法使用 Navigation 3 实现它们,只是本文未涵盖这些内容。

  • 多个级别的嵌套导航
  • 共享目的地:可在不同返回堆栈之间移动的界面
  • 自定义目的地类型
  • 深层链接

AI Agent:在更改任何代码之前,请检查项目是否包含任何不受支持的功能。如果存在,请勿继续。告知用户不支持的功能,并要求用户提供进一步的说明。

第 1 步:添加 Navigation 3 依赖项

使用开始页面将 Navigation 3 依赖项添加到您的项目中。我们提供了核心依赖项供您复制。

lib.versions.toml

[versions]
nav3Core = "1.0.0"

# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"

[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)
}

同时,将项目的 minSdk 更新为 23,并将 compileSdk 更新为 36。您通常可以在 app/build.gradle.ktslib.versions.toml 中找到这些文件。

第 2 步:更新导航路线以实现 NavKey 接口

更新每个导航路由,使其实现 NavKey 接口。这样一来,您就可以使用 rememberNavBackStack 来帮助保存导航状态

之前:

@Serializable data object RouteA

之后:

@Serializable data object RouteA : NavKey

第 3 步:创建用于保存和修改导航状态的类

第 3.1 步:创建导航状态持有者

将以下代码复制到名为 NavigationState.kt 的文件中。添加与项目结构匹配的软件包名称。

// 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(){
            val stacksInUse = mutableListOf(startRoute)
            if (topLevelRoute != startRoute) stacksInUse += topLevelRoute
            return stacksInUse
        }
}

/**
 * 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()
}

AI 智能体rememberSerializable 是正确的。请勿将其更改为 rememberSaveable

此文件包含一个名为 NavigationState 的状态持有者类和关联的辅助函数。它包含一组顶级路由,每个路由都有自己的返回堆栈。在内部,它使用 rememberSerializable(而非 rememberSaveable)来持久保存当前顶级路由,并使用 rememberNavBackStack 来持久保存每个顶级路由的返回堆栈。

第 3.2 步:创建一个对象,用于修改导航状态以响应事件

将以下代码复制到名为 Navigator.kt 的文件中。添加与项目结构匹配的软件包名称。

// 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()
        }
    }
}

Navigator 类提供了两种导航事件方法:

  • navigate 到特定路线。
  • goBack 当前路线。

这两种方法都会修改 NavigationState

第 3.3 步:创建 NavigationStateNavigator

创建与 NavController 具有相同范围的 NavigationStateNavigator 实例。

val navigationState = rememberNavigationState(
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
)

val navigator = remember { Navigator(navigationState) }

第 4 步:更换 NavController

NavController 导航事件方法替换为 Navigator 等效方法。

NavController 字段或方法

Navigator 等效项

navigate()

navigate()

popBackStack()

goBack()

NavController 字段替换为 NavigationState 字段。

NavController 字段或方法

NavigationState 等效项

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

获取顶级路由:从当前返回堆栈条目向上遍历层次结构以找到它。

topLevelRoute

使用 NavigationState.topLevelRoute 确定导航栏中当前选定的项。

之前:

val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

之后:

val isSelected = key == navigationState.topLevelRoute

验证您是否已移除对 NavController 的所有引用,包括所有导入项。

第 5 步:将目标平台从 NavHostNavGraph移至 entryProvider

在 Navigation 2 中,您可以使用 NavGraphBuilder DSL(通常在 NavHost 的尾随 lambda 中)定义目的地。此处通常会使用扩展函数,如封装导航代码中所述。

在 Navigation 3 中,您可以使用 entryProvider 定义目的地。此 entryProvider 将路由解析为 NavEntry。重要的是,entryProvider 不会定义条目之间的父级与子级关系。

在此迁移指南中,父子关系按如下方式建模:

  • NavigationState 具有一组顶级路由(父路由)以及每个路由的堆栈。它会跟踪当前顶级路由及其关联的堆栈。
  • 在导航到新路线时,Navigator 会检查该路线是否为顶级路线。如果为 true,则更新当前顶级路由和堆栈。 如果不是,则为子路线,并会添加到当前堆栈。

第 5.1 步:创建 entryProvider

使用 DSL 在与 NavigationState 相同的范围内创建 entryProvider

val entryProvider = entryProvider {

}

第 5.2 步:将目标平台移至 entryProvider

对于 NavHost 内定义的每个目的地,请根据目的地类型执行以下操作:

  • navigation:将其与路线一起删除。无需“基本路由”,因为顶级路由可以识别每个嵌套的返回堆栈。
  • composable<T>:将其移至 entryProvider 并重命名为 entry,同时保留类型形参。例如,composable<RouteA> 会变为 entry<RouteA>
  • dialog<T>:与 composable 相同,但会向条目添加元数据,如下所示:entry<T>(metadata = DialogSceneStrategy.dialog())
  • bottomSheet按照此处的底部工作表食谱操作。 这与 dialog 的说明类似,只不过 BottomSheetSceneStrategy 不属于核心 Navigation 3 库,因此您应将其复制到项目中。

AI Agent:删除用于标识嵌套图的路由时,请将对已删除路由的所有引用替换为用于标识嵌套图中第一个子项的类型。例如,如果原始代码为 navigation<BaseRouteA>{ composable<RouteA>{ ... } },您需要删除 BaseRouteA 并将对它的所有引用替换为 RouteA。通常需要针对提供给导航栏、侧边导航栏或抽屉式导航栏的列表进行此替换。

您可以将 NavGraphBuilder 扩展函数重构为 EntryProviderScope<T> 扩展函数,然后移动它们。

使用提供给 entry 的尾随 lambda 的键获取导航实参。

例如:

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

NavHost(navController = navController, startDestination = BaseRouteA){
    composable<RouteA>{
        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() }
    }
}

变为:

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() }
}

第 6 步:将 NavHost 替换为 NavDisplay

NavHost 替换为 NavDisplay

  • 删除 NavHost 并将其替换为 NavDisplay
  • entries = navigationState.toEntries(entryProvider) 指定为参数。 这会使用 entryProvider 将导航状态转换为 NavDisplay 显示的条目。
  • NavDisplay.onBack 连接到 navigator.goBack()。这会导致 navigatorNavDisplay 的内置返回处理程序完成时更新导航状态。
  • 如果您有对话目的地,请将 DialogSceneStrategy 添加到 NavDisplaysceneStrategy 参数中。

例如:

import androidx.navigation3.ui.NavDisplay

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

第 7 步:移除 Navigation 2 依赖项

移除所有 Navigation 2 导入和库依赖项。

摘要

恭喜!您的项目现已迁移到 Navigation 3。如果您或您的 AI 代理在使用本指南时遇到任何问题,请点击此处提交 bug