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 的生命周期不对。
二、传递参数:简单类型与复杂对象
导航不带参数,那跟静态页面有什么区别?实际项目中,你几乎每次跳转都要带点数据。
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")
Uri.encode() 编码。否则如果对象里有特殊字符(比如 &、?、空格),路由解析会直接报错。这个坑我踩过两次,现在每次传 JSON 都会条件反射地加 encode。
三、嵌套导航:模块化你的路由
当你的 App 页面多起来之后,把所有路由写在一个 NavHost 里会变得非常臃肿。这时候就需要嵌套导航了。
说白了,嵌套导航就是把一组相关的页面放到一个子图中,子图有自己的 NavHost 和 NavController。这样做的好处是:
- 每个模块的路由独立管理,互不干扰
- 可以单独处理返回栈
- 方便团队协作,每人负责一个子图
来看一个实际例子。假设你的 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 导航知识结构,你可以把它当作一个快速索引:
五、避坑与最佳实践
最后分享几个我在实战中总结出来的经验:
- 不要滥用嵌套。如果你的子图只有两三个页面,强行嵌套反而增加了复杂度。我一般以「功能模块」为单位,一个模块至少 4 个页面以上才考虑嵌套。
- 参数尽量精简。能传 ID 就别传整个对象。传 ID 的好处是:即使对象数据变了,页面也能从本地数据库或网络重新拉取。
- 注意返回栈行为。有时候你从 A → B → C,按返回键想直接回 A,而不是 B。这时候可以用
navController.navigate("C") { popUpTo("A") }来控制。 - 深链接支持。如果你的 App 需要从外部通知或 URL 跳转到某个页面,记得在
NavHost里配置deepLinks。这个我在推送通知场景里用过,效果很好。
object Routes { const val HOME = "home" }。这样改路由名的时候,改一处就行,不会出现字符串散落在各个文件里的情况。
好了,Compose 导航的核心内容就这些。从基本用法到参数传递,再到嵌套导航,每一步都有它的应用场景。你可以在自己的项目里试着把 Fragment 导航迁移过来,感受一下声明式 UI 的流畅体验。
公众号:蓝海资料掘金营,微信deep3321