ArturoYi
Android
从依赖、类型安全路由到 NavHost、跳转、读参、返回栈与深层链接,按任务顺序在 Jetpack Compose 中接入 Navigation。

用 Jetpack Navigation Compose(2.8+)的类型安全 API 在页面之间跳转:用 @Serializable 路由替代字符串 path,在编译期发现拼写和参数类型错误。

本文覆盖 Navigation 2 的 Compose 类型安全写法,这是当前多数项目的生产路径。Google 另有面向 Compose 的 Navigation 3,新项目可以对照官方文档评估,不必和本文的 NavHost API 混用。

添加依赖

类型安全路由依赖两件事:navigation-compose 2.8.0 及以上,以及 Kotlin Serialization 插件与 JSON 运行时。先在 Version Catalog 里固定版本,再在 app 模块启用插件并声明依赖。版本号以 AndroidX Navigation 发布说明 为准。

gradle/libs.versions.toml
[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 模块应用插件并引入库:

app/build.gradle.kts
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。参数只放轻量、可序列化的值(原始类型、枚举、短字符串),不要把整个领域模型塞进路由。

navigation/AppRoute.kt
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 把路由收拢到一处,避免散落的顶层类型:

navigation/AppRoute.kt
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 字符串。

navigation/AppNavHost.kt
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。

ui/home/HomeScreen.kt
fun onOpenProfile(userId: String) {
    navController.navigate(Profile(userId = userId))
}

fun onOpenHomeAsRoot() {
    navController.navigate(Home) {
        popUpTo<Home> { inclusive = true }
        launchSingleTop = true
    }
}

判断当前目的地时不要再比较字符串。使用 hasRoute:

ui/scaffold/AppScaffold.kt
import androidx.navigation.hasRoute

val isHome = navController.currentBackStackEntry
    ?.destination
    ?.hasRoute<Home>() == true

读取导航参数

在 Composable 中读取

在 composable<T> 的 lambda 里对 NavBackStackEntry 调用 toRoute<T>(),得到与路由定义一致的对象。

navigation/AppNavHost.kt
composable<Profile> { backStackEntry ->
    val profile = backStackEntry.toRoute<Profile>()
    ProfileScreen(userId = profile.userId)
}

在 ViewModel 中读取

把同样的路由从 SavedStateHandle 解出来,进程被杀后系统恢复页面时参数仍然在。

ui/profile/ProfileViewModel.kt
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
}
Composable 和 ViewModel 都要从路由读 ID,不要一边 toRoute()、一边再手写 savedStateHandle["userId"]。两套取值方式并存时,重构参数名很容易漏改其中一处。

处理返回栈

弹出上一页

系统返回手势、导航栏 Up 和你自己的「返回」按钮应对齐到同一套 API:普通返回用 popBackStack(),工具栏 Up 用 navigateUp()(会考虑深层链接进入时的父图)。

ui/profile/ProfileScreen.kt
fun onBack() {
    navController.popBackStack()
}

fun onNavigateUp() {
    navController.navigateUp()
}

清空返回栈后跳转

登录成功、完成引导这类「不应再回到上一页」的场景,先 popUpTo 起始目的地并设 inclusive,再 navigate。launchSingleTop 避免栈顶已经是目标时再压一层。

ui/login/LoginScreen.kt
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。

navigation/AppNavHost.kt
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)
}
app/src/main/AndroidManifest.xml
<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 里
Copyright © 2026