前言
Navigation Compose 刚出来时,路由全靠字符串拼。navigate("detail/$userId") 里 userId 是字符串还是 Int、是 String 还是 Long,编译器不帮你检查。拼错了运行时崩,而且只在用户点进去那一下才暴露。
Navigation 2.8.0+ 终于引入了类型安全导航(Type-Safe Navigation)。用 @Serializable 对象做路由,编译器帮你查参数类型,IDE 自动补全。
一、旧方案的问题
// 旧方式:字符串路由
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen() }
composable("detail/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
// userId 可能是 null,类型不确定
DetailScreen(userId)
}
}
// 导航调用
navController.navigate("detail/abc123") // 字符串拼错怎么办?
问题清单:
userId参数名写错(userIDvsuserId)-> 运行时 null参数类型不确定(String 还是 Int?)-> 需要手动 parse
没有编译时检查,拼错一个字符运行时才崩
IDE 不补全路由名
二、类型安全导航
加依赖
dependencies {
implementation("androidx.navigation:navigation-compose:2.8.5")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
// build.gradle.kts
plugins {
kotlin("plugin.serialization")
}
定义路由
// 无参路由:用 object
@Serializable
object Home
// 带参路由:用 data class
@Serializable
data class Detail(val userId: String)
@Serializable
data class EditProfile(
val userId: String,
val section: String,
val fromEdit: Boolean = false
)
// 嵌套路由:父路由
@Serializable
object ProfileGraph
@Serializable
object Settings
路由定义就是普通的 Kotlin 对象,注解 @Serializable 让 Navigation 能序列化/反序列化它们。
注册导航图
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Home // 直接传对象,不再是字符串
) {
composable<Home> {
HomeScreen(
onUserClick = { user ->
// 类型安全导航:传 Detail 对象
navController.navigate(Detail(userId = user.id))
}
)
}
composable<Detail> { backStackEntry ->
// 类型安全取参数
val args: Detail = backStackEntry.toRoute()
DetailScreen(
userId = args.userId,
onEdit = {
navController.navigate(EditProfile(
userId = args.userId,
section = "profile"
))
}
)
}
composable<EditProfile> { backStackEntry ->
val args: EditProfile = backStackEntry.toRoute()
EditProfileScreen(
userId = args.userId,
section = args.section
)
}
}
}
对比旧方式,关键变化:
composable<Home>替代composable("home")-> 泛型约束navController.navigate(Detail("123"))替代navigate("detail/123")-> 编译时检查backStackEntry.toRoute<Detail>()替代arguments?.getString("userId")-> 类型安全取参
三、参数类型支持
@Serializable 的字段类型受 Navigation 支持:
@Serializable
enum class SortOrder { ASC, DESC }
@Serializable
data class ProductList(
val category: String,
val page: Int, // Int 类型,编译器保证不是 String
val showAvailable: Boolean,
val sort: SortOrder
)
// 使用
navController.navigate(ProductList(
category = "electronics",
page = 1,
showAvailable = true,
sort = SortOrder.ASC
))
如果传错类型,编译直接报错:
// 编译错误:type mismatch
navController.navigate(ProductList(
category = "electronics",
page = "1", // 错:String 不能赋给 Int
showAvailable = true,
sort = SortOrder.ASC
))
四、嵌套导航图
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController, startDestination = Home) {
composable<Home> { HomeScreen() }
// 嵌套图
navigation<ProfileGraph>(startDestination = Settings) {
composable<Settings> { SettingsScreen() }
composable<Detail>("userId") { backStackEntry ->
val args = backStackEntry.toRoute<Detail>()
DetailScreen(args.userId)
}
}
}
}
navigation<> 也支持类型安全的方式定义嵌套图。
五、Deep Link 支持
类型安全路由的 Deep Link 定义:
composable<Detail>(
deepLinks = listOf(
navDeepLink {
// URI 模板
uriPattern = "myapp://detail/{userId}"
}
)
) { backStackEntry ->
val args = backStackEntry.toRoute<Detail>()
DetailScreen(args.userId)
}
外部 Intent 触发 myapp://detail/abc123 时,Navigation 自动解析出 Detail(userId="abc123")。
六、迁移策略
不建议一次性迁移所有路由,推荐渐进式:
// 混合模式:旧路由和新路由共存
NavHost(navController, startDestination = "home") {
// 旧路由保留
composable("home") { HomeScreen() }
composable("old_detail/{id}") { /* ... */ }
// 新路由逐步加入
composable<Detail> { backStackEntry ->
val args = backStackEntry.toRoute<Detail>()
DetailScreen(args.userId)
}
}
// 导航调用时按需切换
fun goDetail(navController: NavController, userId: String) {
// 旧: navController.navigate("detail/$userId")
// 新:
navController.navigate(Detail(userId))
}
迁移优先级建议:
参数多的路由先迁(受益最大)
参数类型复杂(Int/Boolean/Enum)的先迁
字符串路由最容易拼错的先迁
七、性能对比
类型安全导航的开销主要在序列化/反序列化。实测:
多出来的 0.1ms 是 JSON 序列化开销,对用户体验不可感知。
小结
类型安全导航把"运行时才发现的拼路由 bug"提前到了编译时。改造成本低(加注解 + 改泛型),收益高(减少调试时间 + IDE 补全)。
新项目直接用类型安全导航。老项目按高频路由渐进迁移。
下篇进入专家级:Android Privacy Sandbox,广告归因体系正在被 Google 重构。