20、导航与Compose:在Compose中使用Navigation、NavHost与composable()、Compose中的参数传递

各位同学,今天我们来聊聊 Compose 中的导航。说实话,Jetpack Navigation 在传统 View 体系里已经非常成熟了,但到了 Compose 时代,它的用法有了不小的变化。我个人觉得,理解好这套机制,是掌握单 Activity 架构的关键一步。

为什么要在 Compose 里用 Navigation?

你想想看,单 Activity 架构的核心是什么?说白了就是「一个 Activity,多个 Fragment 或 Composable 页面」。在 Compose 中,我们不再需要 FragmentManager 那一套了,取而代之的是 NavHostcomposable() 函数。

我在项目中遇到过不少团队,刚开始用 Compose 时还是习惯用 Fragment 做页面切换,结果代码越写越重。其实 Compose 的导航设计得非常轻量,你只需要定义好路由,剩下的交给框架就好。

核心概念:

  • NavController:导航控制器,负责管理页面栈
  • NavHost:导航宿主,用来承载页面内容
  • composable():注册页面的函数,类似 Fragment 的导航图

基础用法:三步搞定导航

我们先看一个最简单的例子。假设你有两个页面:首页和详情页。

// 第一步:创建 NavController
val navController = rememberNavController()

// 第二步:定义 NavHost
NavHost(
    navController = navController,
    startDestination = "home"
) {
    // 第三步:注册页面
    composable("home") {
        HomeScreen(
            onNavigateToDetail = { id ->
                navController.navigate("detail/$id")
            }
        )
    }
    composable(
        route = "detail/{itemId}",
        arguments = listOf(
            navArgument("itemId") { type = NavType.StringType }
        )
    ) { backStackEntry ->
        val itemId = backStackEntry.arguments?.getString("itemId")
        DetailScreen(itemId = itemId)
    }
}

嗯,这里要注意:startDestination 是必须指定的,它决定了应用启动时显示哪个页面。我刚开始用的时候经常忘记写这个,结果页面一片空白,排查了半天才发现是这个问题。

参数传递:不只是字符串

Compose 导航的参数传递比 Fragment 要灵活得多。你可以传递基本类型,也可以传递序列化对象。但说实话,我建议尽量用基本类型,因为复杂对象的传递容易出问题。

参数类型 路由定义 传递方式
String detail/{id} navigate("detail/abc")
Int detail/{id} navigate("detail/123")
Boolean detail?isPremium={isPremium} navigate("detail?isPremium=true")
自定义对象 推荐使用 NavType 序列化 通过 navArgument 配置

我的经验:如果你需要传递复杂对象,可以考虑用 ViewModel 共享数据,或者用 SavedStateHandle。我曾经在项目里直接传了一个大 JSON 字符串,结果导航动画卡顿得不行。后来改成传 ID,在目标页面通过 ViewModel 加载数据,流畅多了。

深层链接与返回栈管理

Compose 的 Navigation 也支持深层链接(Deep Link)。这个功能在推送通知和外部链接跳转时特别有用。

composable(
    route = "profile/{userId}",
    deepLinks = listOf(
        navDeepLink { uriPattern = "myapp://profile/{userId}" }
    )
) { backStackEntry ->
    val userId = backStackEntry.arguments?.getString("userId")
    ProfileScreen(userId = userId)
}

返回栈管理也是重点。默认情况下,navigate() 会把新页面压入栈顶。但有时候我们需要清除返回栈,比如登录成功后跳转到主页。

navController.navigate("home") {
    // 清除返回栈中所有页面,除了 "home"
    popUpTo("login") { inclusive = true }
    // 避免重复创建 "home" 页面
    launchSingleTop = true
}

我曾经犯过一个错误:登录成功后没有清除返回栈,用户按返回键又回到了登录页。嗯,这种体验非常糟糕。所以记得用 popUpTo 来管理好返回栈。

Compose 导航的常见坑

我总结几个实际项目中容易踩的坑,给大家提个醒:

  • 重复导航:快速点击按钮时,可能会触发多次 navigate()。解决方案是用 launchSingleTop = true 或者在点击事件里做防抖。
  • 参数类型不匹配:路由中的参数类型必须和 navArgument 定义的一致。比如你定义了 Int 类型,传了字符串就会崩溃。
  • 状态丢失:Compose 页面重建时,导航状态可能会丢失。建议用 rememberSaveable 保存关键状态。

避坑指南:我曾经在项目里用 navigate() 传递了一个很大的列表数据,结果应用直接 OOM 了。后来我改用 Room 数据库 + ID 传递的方式,既安全又高效。记住:导航参数不是用来传大量数据的,它只是路由信息。

知识体系总览

下面这张图是我自己整理的 Compose 导航知识体系,你可以对照着复习:

Compose 导航知识体系 NavController 路由定义:composable("route/{arg}") 参数传递:navArgument + NavType + 路由占位符 导航操作:navigate() / popBackStack() / popUpTo() 高级特性:Deep Link / 返回栈管理 / 动画过渡 基础 核心 进阶 高阶

实战建议

最后,我想给大家一些实战中的建议:

  • 统一管理路由:建议把所有路由字符串定义在一个常量类里,避免硬编码。我习惯用 sealed class 来封装路由。
  • 测试导航逻辑:Compose 的 Navigation 是可以单元测试的。用 NavHostController 的测试 API 可以验证页面跳转是否正确。
  • 性能优化:如果页面层级很深,考虑用 launchSingleTop 避免重复创建。另外,composable() 中的内容应该尽量轻量。

好了,关于 Compose 导航的内容就讲到这里。记住,导航是应用的骨架,设计好了,后面的开发会顺畅很多。如果你在实际项目中遇到什么问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321