44、Compose导航:NavHost在Compose中、传递参数、嵌套导航

导航,说白了就是页面之间的跳转逻辑。在传统的 Android 开发里,我们用 Fragment + NavController 那一套。到了 Compose 时代,Google 推出了 NavHost,专门为声明式 UI 量身定制。

我个人习惯把 Compose 导航拆成三个层次来理解:路由定义参数传递嵌套结构。今天咱们就一层层把它说透。

一、NavHost 的基本用法

先看一个最简单的例子。你有一个首页,一个详情页,怎么跳?

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToDetail = {
                    navController.navigate("detail")
                }
            )
        }
        composable("detail") {
            DetailScreen()
        }
    }
}

嗯,这里要注意:rememberNavController() 必须在 NavHost 的同一层级或者更上层创建。我在项目中遇到过有人把它写在某个子 Composable 里,结果导航一触发就崩溃——因为 NavController 的生命周期不对。

小技巧: 建议把 NavController 放在最顶层的 Composable 中,比如 MainActivity 的 setContent 里。这样所有子页面都能共享同一个导航实例。

二、传递参数:简单类型与复杂对象

导航不带参数,那跟静态页面有什么区别?实际项目中,你几乎每次跳转都要带点数据。

2.1 基本类型参数

Compose 导航支持直接传 String、Int、Boolean 等基本类型。你只需要在路由里定义占位符:

NavHost(navController, startDestination = "home") {
    composable(
        route = "detail/{itemId}",
        arguments = listOf(
            navArgument("itemId") { type = NavType.IntType }
        )
    ) { backStackEntry ->
        val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
        DetailScreen(itemId = itemId)
    }
}

跳转的时候这样写:

navController.navigate("detail/42")

是不是很简洁?但有个坑——参数名必须和路由占位符一致。我曾经手滑把 itemId 写成了 itemID,结果参数一直拿不到,排查了半天才发现是大小写问题。

2.2 复杂对象参数

传一个数据类怎么办?官方推荐的做法是用 JSON 序列化成 String 再传。我个人习惯用 Gson 或者 Kotlinx Serialization。

data class Product(val id: Int, val name: String, val price: Double)

// 路由定义
composable(
    route = "product/{productJson}",
    arguments = listOf(
        navArgument("productJson") { type = NavType.StringType }
    )
) { backStackEntry ->
    val json = backStackEntry.arguments?.getString("productJson") ?: return@composable
    val product = Gson().fromJson(json, Product::class.java)
    ProductScreen(product = product)
}

// 跳转
val product = Product(1, "Kotlin 实战", 59.0)
val json = Uri.encode(Gson().toJson(product))
navController.navigate("product/$json")
注意: 一定要对 JSON 字符串做 Uri.encode() 编码。否则如果对象里有特殊字符(比如 &、?、空格),路由解析会直接报错。这个坑我踩过两次,现在每次传 JSON 都会条件反射地加 encode。

三、嵌套导航:模块化你的路由

当你的 App 页面多起来之后,把所有路由写在一个 NavHost 里会变得非常臃肿。这时候就需要嵌套导航了。

说白了,嵌套导航就是把一组相关的页面放到一个子图中,子图有自己的 NavHostNavController。这样做的好处是:

  • 每个模块的路由独立管理,互不干扰
  • 可以单独处理返回栈
  • 方便团队协作,每人负责一个子图

来看一个实际例子。假设你的 App 有「商品模块」和「个人中心模块」:

// 主导航
NavHost(navController, startDestination = "main") {
    composable("main") {
        MainScreen(
            onNavigateToProduct = { navController.navigate("productGraph") },
            onNavigateToProfile = { navController.navigate("profileGraph") }
        )
    }

    // 商品子图
    navigation(
        route = "productGraph",
        startDestination = "productList"
    ) {
        composable("productList") { ProductListScreen() }
        composable("productDetail/{id}") { /* ... */ }
    }

    // 个人中心子图
    navigation(
        route = "profileGraph",
        startDestination = "profileHome"
    ) {
        composable("profileHome") { ProfileHomeScreen() }
        composable("settings") { SettingsScreen() }
    }
}

你想想看,如果所有页面都平铺在同一个层级,光是维护路由字符串就够头疼的。嵌套之后,每个子图就像一个独立的小 App,清爽多了。

核心要点: 嵌套导航的 route 是子图的入口标识,startDestination 是子图内部的起始页面。两者不要搞混。

四、知识体系总览

下面这张图是我自己总结的 Compose 导航知识结构,你可以把它当作一个快速索引:

Compose 导航 路由定义 composable("home") { ... } navigation("graph") { ... } 参数传递 基本类型:Int, String 复杂对象:JSON 序列化 嵌套导航 子图独立管理 返回栈隔离 避坑指南 参数名大小写一致 | JSON 记得 Uri.encode

五、避坑与最佳实践

最后分享几个我在实战中总结出来的经验:

  1. 不要滥用嵌套。如果你的子图只有两三个页面,强行嵌套反而增加了复杂度。我一般以「功能模块」为单位,一个模块至少 4 个页面以上才考虑嵌套。
  2. 参数尽量精简。能传 ID 就别传整个对象。传 ID 的好处是:即使对象数据变了,页面也能从本地数据库或网络重新拉取。
  3. 注意返回栈行为。有时候你从 A → B → C,按返回键想直接回 A,而不是 B。这时候可以用 navController.navigate("C") { popUpTo("A") } 来控制。
  4. 深链接支持。如果你的 App 需要从外部通知或 URL 跳转到某个页面,记得在 NavHost 里配置 deepLinks。这个我在推送通知场景里用过,效果很好。
我的习惯: 所有路由字符串统一定义在一个常量文件里,比如 object Routes { const val HOME = "home" }。这样改路由名的时候,改一处就行,不会出现字符串散落在各个文件里的情况。

好了,Compose 导航的核心内容就这些。从基本用法到参数传递,再到嵌套导航,每一步都有它的应用场景。你可以在自己的项目里试着把 Fragment 导航迁移过来,感受一下声明式 UI 的流畅体验。


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