将 Jetpack Navigation 迁移到 Navigation Compose

The Navigation Compose API 允许您在 Compose 应用程序中导航不同可组合项,同时利用 Jetpack Navigation 的组件、基础结构和功能。

此页面描述了如何在更大范围的 View-based UI 向 Jetpack Compose 迁移中,从基于 Fragment 的 Jetpack Navigation 迁移到 Navigation Compose。

迁移先决条件

您可以在能够用相应的屏幕可组合项替换所有 Fragment 后迁移到 Navigation Compose。屏幕可组合项可以包含 Compose 和 View 内容的混合,但所有导航目标都必须是可组合项,才能启用 Navigation Compose 迁移。在此之前,您应该在互操作 View 和 Compose 代码库中继续使用 基于 Fragment 的 Navigation 组件。有关更多信息,请参阅 导航互操作文档

在仅 Compose 的应用程序中使用 Navigation Compose 不是先决条件。您可以继续使用 基于 Fragment 的 Navigation 组件,只要您为 托管可组合内容 保留 Fragment。

迁移步骤

无论您是遵循我们 推荐的迁移策略 还是采用其他方法,您最终都会到达所有导航目标都是屏幕可组合项,Fragment 仅充当可组合容器的阶段。在这个阶段,您可以迁移到 Navigation Compose。

如果您的应用程序已经遵循 UDF 设计模式 和我们的 架构指南,迁移到 Jetpack Compose 和 Navigation Compose 不应该需要对应用程序的其他层进行重大重构,除了 UI 层。

要迁移到 Navigation Compose,请执行以下步骤

  1. Navigation Compose 依赖项 添加到您的应用程序。
  2. 创建一个 App-level 可组合项,并将其添加到您的 Activity 中,作为您的 Compose 入口点,替换 View 布局的设置

    class SampleActivity : ComponentActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // setContentView<ActivitySampleBinding>(this, R.layout.activity_sample)
            setContent {
                SampleApp(/* ... */)
            }
        }
    }

  3. 为每个导航目标创建类型。对于不需要任何数据的目标,使用 data object;对于需要数据的目标,使用 data classclass

    @Serializable data object First
    @Serializable data class Second(val id: String)
    @Serializable data object Third
    

  4. 在所有需要引用它的可组合项都可以访问的位置设置 NavController(这通常是在您的 App 可组合项中)。这种方法遵循 状态提升原则,并允许您使用 NavController 作为导航不同可组合屏幕和维护回退堆栈的真实来源

    @Composable
    fun SampleApp() {
        val navController = rememberNavController()
        // ...
    }

  5. App 可组合项中创建应用程序的 NavHost,并传递 navController

    @Composable
    fun SampleApp() {
        val navController = rememberNavController()
    
        SampleNavHost(navController = navController)
    }
    
    @Composable
    fun SampleNavHost(
        navController: NavHostController
    ) {
        NavHost(navController = navController, startDestination = First) {
            // ...
        }
    }

  6. 添加 composable 目标来构建您的导航图。如果每个屏幕以前都已迁移到 Compose,此步骤只需将这些屏幕可组合项从 Fragment 中提取到 composable 目标中

    class FirstFragment : Fragment() {
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            return ComposeView(requireContext()).apply {
                setContent {
                    // FirstScreen(...) EXTRACT FROM HERE
                }
            }
        }
    }
    
    @Composable
    fun SampleNavHost(
        navController: NavHostController
    ) {
        NavHost(navController = navController, startDestination = First) {
            composable<First> {
                FirstScreen(/* ... */) // EXTRACT TO HERE
            }
            composable<Second> {
                SecondScreen(/* ... */)
            }
            // ...
        }
    }

  7. 如果您遵循了有关 构建 Compose UI 的架构 的指导,特别是关于如何将 ViewModel 和导航事件传递给可组合项的指导,下一步是更改将 ViewModel 提供给每个屏幕可组合项的方式。您通常可以使用 Hilt 注入,以及它通过 hiltViewModel 与 Compose 和 Navigation 的集成点

    @Composable
    fun FirstScreen(
        // viewModel: FirstViewModel = viewModel(),
        viewModel: FirstViewModel = hiltViewModel(),
        onButtonClick: () -> Unit = {},
    ) {
        // ...
    }

  8. navController 替换所有 findNavController() 导航调用,并将这些调用作为导航事件传递给每个可组合屏幕,而不是传递整个 navController。这种方法遵循 最佳实践,即从可组合函数中向调用者公开事件,并保持 navController 作为唯一的真实来源。

    可以通过创建该目标的路由类实例来将数据传递到目标。然后,可以通过目标处的回退堆栈条目直接获取该实例,也可以使用 SavedStateHandle.toRoute()ViewModel 获取该实例。

    @Composable
    fun SampleNavHost(
        navController: NavHostController
    ) {
        NavHost(navController = navController, startDestination = First) {
            composable<First> {
                FirstScreen(
                    onButtonClick = {
                        // findNavController().navigate(firstScreenToSecondScreenAction)
                        navController.navigate(Second(id = "ABC"))
                    }
                )
            }
            composable<Second> { backStackEntry ->
                val secondRoute = backStackEntry.toRoute<Second>()
                SecondScreen(
                    id = secondRoute.id,
                    onIconClick = {
                        // findNavController().navigate(secondScreenToThirdScreenAction)
                        navController.navigate(Third)
                    }
                )
            }
            // ...
        }
    }

  9. 删除所有 Fragment、相关的 XML 布局、不必要的导航和其他资源,以及过时的 Fragment 和 Jetpack Navigation 依赖项。

您可以在 设置文档 中找到包含更多 Navigation Compose 相关详细信息的相同步骤。

常见用例

无论您使用的是哪个 Navigation 组件,相同的导航原则都适用

迁移时常见的用例包括:

有关这些用例的更详细的信息,请参阅 使用 Compose 进行导航

导航时检索复杂数据

我们强烈建议不要在导航时传递复杂的数据对象。相反,在执行导航操作时,请将最少必要的信息(例如唯一标识符或其他形式的 ID)作为参数传递。您应该将复杂对象作为数据存储在唯一的真实来源中,例如 数据层。有关更多信息,请参阅 导航时检索复杂数据

如果您的 Fragment 将复杂对象作为参数传递,请先考虑重构代码,以允许从数据层存储和获取这些对象。有关示例,请参阅 Now in Android 代码库

限制

本节介绍 Navigation Compose 的当前限制。

增量迁移到 Navigation Compose

目前,您无法在代码中仍然使用 Fragment 作为目标时使用 Navigation Compose。要开始使用 Navigation Compose,所有目标都需要是可组合项。您可以跟踪 问题跟踪器上的功能请求

过渡动画

Navigation 2.7.0-alpha01 开始,现在直接在 NavHost 中支持设置自定义过渡,以前是从 AnimatedNavHost 中设置的。有关更多信息,请阅读 发行说明

了解更多

有关迁移到 Navigation Compose 的更多信息,请参阅以下资源

  • Navigation Compose 代码实验室:通过动手实践的代码实验室学习 Navigation Compose 的基础知识。
  • Now in Android 代码库:一个完全用 Kotlin 和 Jetpack Compose 构建的功能齐全的 Android 应用程序,它遵循 Android 设计和开发的最佳实践,并且包括 Navigation Compose。
  • 将 Sunflower 迁移到 Jetpack Compose:一篇博客文章,记录了 Sunflower 示例应用程序从 View 迁移到 Compose 的迁移历程,其中还包括迁移到 Navigation Compose。
  • Jetnews for every screen:一篇博客文章,记录了对 Jetnews 示例的重构和迁移,以支持使用 Jetpack Compose 和 Navigation Compose 的所有屏幕。