Compose Navigation 导航与传参
Jetpack Navigation for Compose 实战:NavHost 与 composable 目标、NavController 管理、必选/可选参数传递、嵌套 NavGraph、底部导航栏集成与深度链接 deepLink 接入。
学习目标
- 搭建 NavHost 与 composable 目标,掌握路由与目的地概念
- 熟练使用 NavController 管理转场与回退栈
- 传递必选参数与可选参数(含类型转换与默认值)
- 使用嵌套 NavGraph 组织多级导航
- 集成底部导航栏并接入深度链接 deepLink
学习目标
- 搭建
NavHost与composable目标,掌握路由与目的地概念; - 熟练使用
NavController管理转场与回退栈; - 传递必选参数与可选参数(含类型转换与默认值);
- 使用嵌套
NavGraph组织多级导航; - 集成底部导航栏并接入深度链接 deepLink。
Compose Navigation 导航与传参
androidx.navigation:navigation-compose 把 Navigation 库移植到 Compose:用 NavHost 描述“哪些 composable 是路由目的地”,用 NavController 在它们之间跳转。它仍是基于回退栈的导航,思路与第 13 章的 Intent 导航类似,但更声明式、更适配 Compose 的“状态驱动”思想。
依赖配置
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.09.03")
implementation(composeBom)
implementation("androidx.compose.material3:material3")
implementation("androidx.navigation:navigation-compose:2.8.2")
// 用于在 ViewModel 中通过 SavedStateHandle 读取参数
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
}
NavHost 与 composable 目标
NavHost 是导航容器,它内部渲染“当前目的地对应的 Composable”。每个目的地用 composable("route") { ... } 注册:
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@Composable
fun AppNavHost() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(onOpenDetail = { id ->
navController.navigate("detail/$id")
})
}
composable("detail/{id}") { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
DetailScreen(id)
}
}
}
要点:
rememberNavController()在 Composable 中获取并缓存NavController,配置变更后仍指向同一实例;startDestination是初始路由字符串;composable("route")声明路由对应的可组合函数,块内能拿到NavBackStackEntry,从中读取参数、SavedStateHandle、ViewModel 等。
NavController:跳转与回退
NavController 是导航的操作入口:
// 普通跳转
navController.navigate("detail/42")
// 跳转并配置 popUpTo,避免回退栈无限增长
navController.navigate("home") {
popUpTo("home") { inclusive = true } // 清空到 home(含)
launchSingleTop = true // 栈顶是 home 时复用
}
// 返回
navController.popBackStack()
// 判断能否返回(如根栈底)
if (navController.previousBackStackEntry != null) {
navController.popBackStack()
}
navigate 的可选参数
navController.navigate(route) { ... } 的 lambda 里可以配置:
| 选项 | 作用 |
|---|---|
popUpTo(route) { inclusive } |
跳转前弹出栈直到 route |
launchSingleTop = true |
栈顶已是目标则不重复入栈 |
restoreState = true |
恢复上次弹出的目的地的 SavedState |
enterTransition / exitTransition |
自定义动画 |
经典“底部 Tab 切换”模式:
navController.navigate(tab.route) {
popUpTo(graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
saveState=true/restoreState=true 组合使每个 Tab 的状态独立保留,切换 Tab 时各自的滚动位置/输入内容不会丢。
NavController必须在 Composable 中获取并放在NavHost外层,不要在事件回调里临时rememberNavController()——那样得到的实例不一致。
参数传递
必选参数
在路由模板里用 {} 占位,目的地用 arguments 声明类型:
composable(
route = "detail/{itemId}",
arguments = listOf(navArgument("itemId") { type = NavType.StringType })
) { entry ->
val itemId = entry.arguments?.getString("itemId")
DetailScreen(itemId!!)
}
跳转时把值拼进字符串:
navController.navigate("detail/${item.id}")
可选参数与默认值
可选参数用 ?{name}={value} 形式,并在 navArgument 中给默认值:
composable(
route = "list?filter={filter}&sort={sort}",
arguments = listOf(
navArgument("filter") {
type = NavType.StringType
defaultValue = "all" // 必须有默认值才算可选
nullable = true
},
navArgument("sort") {
type = NavType.IntType
defaultValue = 0
}
)
) { entry ->
val filter = entry.arguments?.getString("filter")
val sort = entry.arguments?.getInt("sort") ?: 0
ListScreen(filter, sort)
}
跳转:
navController.navigate("list?filter=hot&sort=1")
// 或省略可选参数,使用默认值
navController.navigate("list")
URL 中拼接字符串参数要做
URLEncoder.encode(...),避免特殊字符破坏路由。
通过 ViewModel 读取参数
NavBackStackEntry 也是 ViewModelStoreOwner,可以创建目的地级作用域的 ViewModel,并在其中用 SavedStateHandle 直接读取参数:
class DetailViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val itemId: String = checkNotNull(savedStateHandle["itemId"])
// 或动态监听:savedStateHandle.getStateFlow("itemId", "")
}
@Composable
fun DetailScreen(itemId: String) {
val vm: DetailViewModel = viewModel(
factory = SavedStateViewModelFactory(null, /* entry */)
)
// ...
}
在
composable {}块内调用viewModel()会自动绑定到当前NavBackStackEntry,因此不同目的地的 ViewModel 是相互独立的。
嵌套 NavGraph:navigation
复杂应用常把多页面按模块分组。navigation(startDestination, route) 可以在 NavHost 内嵌套一个子图:
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen() }
// 订单模块子图
navigation(startDestination = "order/list", route = "order") {
composable("order/list") { OrderListScreen() }
composable("order/detail/{id}") { entry ->
OrderDetailScreen(entry.arguments?.getString("id"))
}
composable("order/tracking/{id}") { entry ->
TrackingScreen(entry.arguments?.getString("id"))
}
}
// 个人中心子图
navigation(startDestination = "profile/main", route = "profile") {
composable("profile/main") { ProfileScreen() }
composable("profile/settings") { SettingsScreen() }
}
}
跳转到子图入口或子图内任意目的地都允许:
navController.navigate("order") // 进入 order/list
navController.navigate("order/detail/O-001") // 直接进入 tracking
子图的好处:
- 跨模块共享
popUpTo边界(如popUpTo("order") { inclusive = true }一键返回订单列表); - 配合
navigation作为 Tab 根,方便整组切换。
底部导航栏集成
底部导航是导航库最经典的搭配。结构是 Scaffold(bottomBar = { ... }) + NavHost,状态由 Compose 的“当前路由”驱动:
@Composable
fun MainScreen() {
val navController = rememberNavController()
val items = listOf(
BottomItem("home", "首页", Icons.Default.Home),
BottomItem("order/list", "订单", Icons.Default.List),
BottomItem("profile/main", "我的", Icons.Default.Person)
)
val backStack by navController.currentBackStackEntryAsState()
val currentRoute = backStack?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
items.forEach { item ->
NavigationBarItem(
selected = currentRoute == item.route ||
currentRoute?.startsWith(item.route.split("/").first()) == true,
onClick = {
if (currentRoute != item.route) {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
},
icon = { Icon(item.icon, null) },
label = { Text(item.label) }
)
}
}
}
) { padding ->
AppNavHost(navController, Modifier.padding(padding))
}
}
要点:
currentBackStackEntryAsState()把回退栈顶部变成 ComposeState,底部栏自动响应路由变化;- 用
popUpTo + saveState/restoreState实现“切换 Tab 时各自保留状态”; selected计算时要兼容子路由(如order/list与order/detail都属于“订单” Tab)。
深度链接 deepLink
深度链接让外部(通知、其他应用、URL)直接进入指定目的地。Compose Navigation 同时支持 App Links(http/https)和自定义 scheme。
注册 composable 的 deepLinks
composable(
route = "detail/{itemId}",
arguments = listOf(navArgument("itemId") { type = NavType.StringType }),
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://item/{itemId}" },
navDeepLink { uriPattern = "https://example.com/items/{itemId}" }
)
) { entry ->
DetailScreen(entry.arguments?.getString("itemId"))
}
uriPattern 中的 {itemId} 会自动绑定到对应 navArgument。
处理 Intent
在 Activity 中接收 Intent 并交给 NavController:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
AppNavHost(navController)
LaunchedEffect(Unit) {
navController.handleDeepLink(intent)
}
}
}
更完整的做法是用
com.google.androidbrowser:helper+android-app-links验证,但handleDeepLink是 Compose 集成的入口。
Pending Intent:通知点击跳转
fun buildNotificationIntent(context: Context, itemId: String): PendingIntent {
val deepLink = "myapp://item/$itemId".toUri()
val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val bundle = navController.createDeepLink().uri(deepLink).createPendingIntent(flags) as? Bundle
return TaskStackBuilder.create(context).let { it.addNextIntentWithParentStack(intent); it.getPendingIntent(0, flags)!! }
}
简化版可用 NavDeepLinkBuilder:
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.detailFragment)
.setArguments(bundleOf("itemId" to id))
.createPendingIntent()
在纯 Compose 项目里,
nav_graph.xml通常不存在。可以直接构造PendingIntent,让 Intent 带上dataURI,然后在 Activity 里交给navController.handleDeepLink(intent)。
实战:可传参的多模块导航骨架
@Composable
fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(navController, startDestination = "home", modifier = modifier) {
composable("home") {
HomeScreen(
onOpenItem = { id -> navController.navigate("detail/$id") },
onOpenOrders = { navController.navigate("order") }
)
}
composable(
route = "detail/{itemId}",
arguments = listOf(navArgument("itemId") { type = NavType.StringType }),
deepLinks = listOf(navDeepLink { uriPattern = "myapp://item/{itemId}" })
) { entry ->
DetailScreen(
itemId = entry.arguments?.getString("itemId").orEmpty(),
onBack = { navController.popBackStack() }
)
}
navigation(startDestination = "order/list", route = "order") {
composable("order/list") {
OrderListScreen(onOpenDetail = { id -> navController.navigate("order/detail/$id") })
}
composable(
route = "order/detail/{id}",
arguments = listOf(navArgument("id") { type = NavType.StringType })
) { entry ->
OrderDetailScreen(
id = entry.arguments?.getString("id").orEmpty(),
onBack = { navController.popBackStack() }
)
}
}
}
}
外部发送 myapp://item/abc123 即可直达详情页;订单子图内部互相跳转不影响首页栈。
常见坑与最佳实践
- 在事件回调里临时
rememberNavController():得到的实例与NavHost不一致,跳转不生效。NavController必须在 Composable 作用域取得并下传。 - 路由字符串里没占位符但传了参数:
composable("detail")后调用navigate("detail/42")会路由不匹配崩溃。模板与跳转字符串必须严格对应。 - 可选参数没设
defaultValue:被识别为必选,navigate("list")不带参时崩溃。 popUpTo滥用导致栈被清空:根路由popUpTo加inclusive = true会让回退栈空掉,再按返回会直接退出。- 底部 Tab 切换没开
saveState/restoreState:每次切换 Tab 都重建,输入框内容丢失。 - deepLink 的
uriPattern与navArgument名称不一致:参数绑不上,运行时拿到null。 NavController放到 ViewModel:NavController是 UI 层路由器,不要交给架构层。状态导航用路由的StateFlow,UI 层再调用。- 在 Composable 块里直接
navigate:每次重组都触发跳转。应放在事件回调(按钮onClick)里。 - 多个目的地用同一
route:路由冲突,运行时崩溃。路由字符串要全局唯一。 - 未配置 Manifest 的
intent-filter:http/https深度链接在浏览器或其他应用打开时不会被你的 App 拦截。要在AndroidManifest.xml中为 Activity 声明<intent-filter>与android:autoVerify="true"(App Links)。
章节小结
本章覆盖了 Compose 版 Navigation 的全部核心:用 NavHost + composable 描述目的地,用 NavController 跳转与回退;必选参数用 {name} 占位、可选参数用 ?name=value 与 defaultValue;navigation {} 组织嵌套子图便于模块化;底部导航通过 currentBackStackEntryAsState 把路由变化驱动 NavigationBar,saveState/restoreState 保证各 Tab 状态独立;深度链接通过 navDeepLink + handleDeepLink 接入外部入口。配合 ViewModel + SavedStateHandle,导航参数可被目的地级 ViewModel 直接消费。
下一章预告
下一章进入动画与手势领域:animate*AsState、Crossfade、AnimatedContent、updateTransition、Modifier.animateContentSize、rememberInfiniteTransition 等声明式动画 API,以及 pointerInput/detectDragGestures/draggable/swipeable 等手势处理。