初探Navigation
用 Jetpack Navigation Compose(2.8+)的类型安全 API 在页面之间跳转:用 @Serializable 路由替代字符串 path,在编译期发现拼写和参数类型错误。
NavHost API 混用。添加依赖
类型安全路由依赖两件事:navigation-compose 2.8.0 及以上,以及 Kotlin Serialization 插件与 JSON 运行时。先在 Version Catalog 里固定版本,再在 app 模块启用插件并声明依赖。版本号以 AndroidX Navigation 发布说明 为准。
[versions]
navigationCompose = "2.9.0"
kotlinxSerializationJson = "1.8.1"
[libraries]
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
在 app 模块应用插件并引入库:
plugins {
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
}
kotlin-serialization 必须加在定义路由的模块上。只加 kotlinx-serialization-json 而不启用插件,@Serializable 不会生成序列化器,编译会失败。定义类型安全的路由
把每个目的地写成可序列化的 Kotlin 类型,而不是 "home"、"profile/{id}" 这类字符串。无参页面用 object,需要参数的页面用 data class。参数只放轻量、可序列化的值(原始类型、枚举、短字符串),不要把整个领域模型塞进路由。
import kotlinx.serialization.Serializable
@Serializable
object Home
@Serializable
object Login
@Serializable
data class Profile(val userId: String)
@Serializable
data class Search(val query: String = "")
规模变大时,用 sealed interface 把路由收拢到一处,避免散落的顶层类型:
import kotlinx.serialization.Serializable
@Serializable
sealed interface AppRoute {
@Serializable
data object Home : AppRoute
@Serializable
data class Profile(val userId: String) : AppRoute
}
SavedState。不要传递大型列表、Bitmap 或整个网络响应对象;页面之间只传 ID,在目的地用 Repository 或 ViewModel 再取数据。搭建 NavHost 与起始页
在根 Composable 里创建 NavController,用 NavHost 声明图,并把 startDestination 设成起始路由类型。composable<T> 的类型参数就是目的地,不再传 route 字符串。
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
@Composable
fun AppNavHost() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Home,
) {
composable<Home> {
HomeScreen(
onOpenProfile = { userId ->
navController.navigate(Profile(userId = userId))
},
)
}
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(
userId = profile.userId,
onBack = { navController.popBackStack() },
)
}
}
}
rememberNavController() 必须放在会跨配置变更保留的组合位置(通常是 Activity 的根内容)。不要在每个子页面各造一个 controller,否则返回栈会分裂。在页面之间导航
调用 navigate 时传入路由实例。有参目的地直接构造 data class;无参目的地传 object。需要同时清栈或避免重复入栈时,在 navigate 的 lambda 里配置 NavOptions。
fun onOpenProfile(userId: String) {
navController.navigate(Profile(userId = userId))
}
fun onOpenHomeAsRoot() {
navController.navigate(Home) {
popUpTo<Home> { inclusive = true }
launchSingleTop = true
}
}
判断当前目的地时不要再比较字符串。使用 hasRoute:
import androidx.navigation.hasRoute
val isHome = navController.currentBackStackEntry
?.destination
?.hasRoute<Home>() == true
读取导航参数
在 Composable 中读取
在 composable<T> 的 lambda 里对 NavBackStackEntry 调用 toRoute<T>(),得到与路由定义一致的对象。
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.userId)
}
在 ViewModel 中读取
把同样的路由从 SavedStateHandle 解出来,进程被杀后系统恢复页面时参数仍然在。
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.navigation.toRoute
class ProfileViewModel(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
val userId: String = profile.userId
}
toRoute()、一边再手写 savedStateHandle["userId"]。两套取值方式并存时,重构参数名很容易漏改其中一处。处理返回栈
弹出上一页
系统返回手势、导航栏 Up 和你自己的「返回」按钮应对齐到同一套 API:普通返回用 popBackStack(),工具栏 Up 用 navigateUp()(会考虑深层链接进入时的父图)。
fun onBack() {
navController.popBackStack()
}
fun onNavigateUp() {
navController.navigateUp()
}
清空返回栈后跳转
登录成功、完成引导这类「不应再回到上一页」的场景,先 popUpTo 起始目的地并设 inclusive,再 navigate。launchSingleTop 避免栈顶已经是目标时再压一层。
navController.navigate(Home) {
popUpTo<Login> { inclusive = true }
launchSingleTop = true
}
popBackStack() 在栈里只剩根目的地时会返回 false 且不关 Activity。根页的返回行为交给系统(预测性返回 / OnBackPressedDispatcher),不要在根页无条件再 pop 一次。接入深层链接
类型安全 API 用 navDeepLink<T>(basePath = …) 声明 URI,路径参数与路由 data class 的属性对应。同时在 AndroidManifest.xml 为 Activity 配置相同 scheme / host 的 intent-filter,否则系统不会把链接交到你的 App。
import androidx.navigation.navDeepLink
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "https://arturoyi.dev/profile"),
),
) { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.userId)
}
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="arturoyi.dev"
android:pathPrefix="/profile" />
</intent-filter>
</activity>
NavType 只在参数不是 Navigation 内置支持的类型时才需要。String、Int、Boolean、枚举走默认序列化即可;嵌套对象或列表先考虑改成 ID,而不是立刻写一套 JSON NavType。下一步
- 回到 Android 分类 查看同栏目其他笔记
- 对照 类型安全目的地 补齐测试与
hasRoute断言 - 需要多模块、多返回栈或底部导航时,再拆嵌套
navigation<T>图,而不是继续把所有composable堆在一个NavHost里