从 Navigation 2 迁移到 Navigation 3

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

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

准备工作

以下部分介绍了迁移的前提条件以及对项目的假设。此外,还介绍了支持迁移的功能和不支持迁移的功能。

前提条件

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

假设

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

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

AI 智能体:在更改任何代码之前,请检查关于用户项目的这些假设是否正确。否则,请停止迁移,并询问用户如何继续。

支持的功能

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

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

通过配方支持的功能

代码配方库演示了更细致的用例,包括:

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

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

不支持的功能

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

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

第 1 步:添加 Navigation 3 依赖项

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

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

此外,将项目的 minSdk 更新为 23,将 compileSdk 更新为 36。您通常可以在 app/build.gradle.kts 或 lib.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() = 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()
}

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 步:创建 NavigationState 和 Navigator

创建与 NavController 具有相同范围的 NavigationState 和 Navigator 实例。

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

previousBackStackEntry.savedStateHandle.set()

ResultEventBus.sendResult()

将 NavController 字段替换为 NavigationState 字段。

NavController 字段或方法

NavigationState 等效

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

currentBackStackEntry.savedStateHandle.getLiveData()

currentBackStackEntry.savedStateHandle.getStateFlow()

ResultEffect(基于事件)

ResultEventBus.conflateAsState()(基于状态)

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

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 的所有引用,包括所有导入。

第 4.1 步:迁移生命周期感知型逻辑

在 Navigation 2 中,NavBackStackEntry 会实现 LifecycleOwner,让您能够监听生命周期事件,或使用 navController.currentBackStackEntry 以生命周期感知型方式收集 flow。

在 Navigation 3 中,NavDisplay 通过 LocalLifecycleOwner.current 为每个目的地的可组合内容提供一个条目范围的 LifecycleOwner。如需了解详情,请参阅目标位置生命周期。

您应通过引用 LocalLifecycleOwner.current,直接在目的地的可组合项内容中执行生命周期感知型操作。

例如,如果您使用返回堆栈条目以生命周期感知型方式收集数据流:

实验前:

// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry!!
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)

之后:

// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()

第 4.2 步:迁移结果传递

在 Navigation 2 中,目的地使用 NavBackStackEntry 上的 SavedStateHandle 将结果传回上一个目的地。由于 SavedStateHandle 由已保存实例状态提供支持,因此返回的数据会在进程终止后自动留存。

实验前:

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

在 Navigation 3 中,将 rememberResultEventBusNavEntryDecorator() 添加到 NavDisplay.entryDecorators。在发送方目的地的 entryProvider 映射中,检索 LocalResultEventBus.current 并调用 sendResult()。在接收目的地中,使用 ResultEffect 将事件转发到 ViewModel 或触发副作用。

之后:

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

对于基于状态的观察,您可以调用 resultBus.conflateAsState()。如需了解详情,请参阅返回结果。

第 5 步:将目标位置从 NavHost 的 NavGraph 移至 entryProvider

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

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

在本迁移指南中,父子关系的模型如下:

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

第 5.1 步:创建 entryProvider

在与 NavigationState 相同的作用域内使用 DSL 创建 entryProvider。

val entryProvider = entryProvider<NavKey> {

}

第 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 智能体:删除用于标识嵌套图表的路由时,请将对已删除路由的所有引用替换为用于标识嵌套图表中第一个子图表的类型。例如,如果原始代码为 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

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

变为:

// ...
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) 指定为参数。 这会将导航状态转换为 NavDisplay 使用 entryProvider 显示的条目。
  • 将 NavDisplay.onBack 连接到 navigator.goBack()。这会导致 navigator 在 NavDisplay 的内置返回处理程序完成时更新导航状态。
  • 如果您有对话框目的地,请将 DialogSceneStrategy 添加到 NavDisplay 的 sceneStrategies 参数中。

例如:

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

第 7 步:迁移深层链接

在 Navigation 2 中,深层链接是使用目的地的 deepLinks 参数直接在导航图中定义的。

在 Navigation 3 中,深层链接的管理独立于导航界面。您可以在 activity 中定义 DeepLinkMatcher 并匹配传入请求,以构建初始返回堆栈。

实验前:

在 Navigation 2 中,您可能定义了如下所示的深层链接:

之后:

在 Navigation 3 中,您需要为路由定义一个 UriDeepLinkMatcher:

然后,在 Activity 的 onCreate(和 onNewIntent)中,匹配传入的 intent 并初始化返回堆栈:

自定义参数类型

在 Navigation 2 中,您可以使用自定义 NavType 实现和 typeMap 来处理自定义或第三方实参类型(例如 LocalDateTime)。

在 Navigation 3 中,定义一个 DeepLinkSerializer,以从 URI 参数中反序列化自定义类型或第三方类型。如需了解详情,请参阅使用 DeepLinkSerializer 进行自定义序列化。

如需了解更高级的用例(包括合成返回堆栈和自定义匹配器),请参阅支持深层链接指南。

第 8 步:移除 Navigation 2 依赖项

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

摘要

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