Jetpack Compose Navigation 进阶指南:从路由设计到深层链接
Navigation Compose 是 Jetpack Compose 的官方导航解决方案。从 2.8 版本开始引入了类型安全导航 (Type-Safe Navigation),彻底改变了传统字符串路由的写法。本文从项目实战的角度,系统讲解 Compose Navigation 的核心概念和进阶用法。
一、路由设计:项目级方案
导航方案的设计直接影响项目的可维护性。Str ine 拼接路由的方式在项目变大后难以管理。推荐使用 密封类 + 导航图对象 组织路由。
1.1 类型安全路由 (Navigation 2.8+)
Navigation 2.8 引入了基于 Kotlin Serialization 的类型安全导航,告别字符串路由的脆弱性。
// 定义路由
@Serializable
sealed class Screen {
// 无参数路由
@Serializable data object Home : Screen()
@Serializable data object Settings : Screen()
// 带参数路由
@Serializable data class Article(val id: String) : Screen()
@Serializable data class Profile(val userId: String, val tab: String = "posts") : Screen()
}
// NavHost 注册
@Composable
fun AppNavHost() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Home
) {
composable<Screen.Home> { HomeScreen(navController) }
composable<Screen.Settings> { SettingsScreen(navController) }
composable<Screen.Article> { backStackEntry ->
val article: Screen.Article = backStackEntry.toRoute()
ArticleScreen(articleId = article.id)
}
composable<Screen.Profile> { backStackEntry ->
val profile: Screen.Profile = backStackEntry.toRoute()
ProfileScreen(userId = profile.userId, tab = profile.tab)
}
}
}
// 导航调用 - 编译期安全
navController.navigate(Screen.Article(id = "article_123"))
navController.navigate(Screen.Profile(userId = "user_456", tab = "repos"))
1.2 多模块路由方案
在大型项目中,多模块架构下如何组织导航是常见挑战。推荐使用导航图接口解耦:
// feature-home 模块
interface HomeNavigation {
fun navigateToSettings()
fun navigateToArticle(articleId: String)
}
// app 模块 - 统一实现
class AppHomeNavigation(private val navController: NavController) : HomeNavigation {
override fun navigateToSettings() {
navController.navigate(Screen.Settings)
}
override fun navigateToArticle(articleId: String) {
navController.navigate(Screen.Article(id = articleId))
}
}
// feature-home 中使用
@Composable
fun HomeScreen(navigation: HomeNavigation) {
Column {
Button(onClick = { navigation.navigateToSettings() }) {
Text("设置")
}
}
}
这样 feature 模块完全不依赖其他模块的具体路由,只依赖接口。导航的依赖方向始终指向 app 模块。
二、参数传递:不仅仅是基本类型
2.1 支持的参数类型
类型安全导航支持以下参数类型:
| 类型 | 说明 | 默认值支持 |
|---|---|---|
| String | 字符串 | ✅ |
| Int | 整型 | ✅ |
| Long | 长整型 | ✅ |
| Boolean | 布尔值 | ✅ |
| Float | 浮点数 | ✅ |
| Double | 双精度浮点数 | ✅ |
| Parcelable / Serializable | 使用 @Parcelize 注解 | ❌ |
2.2 传递复杂数据
当需要传递的数据超出基本类型时,推荐传递 ID 而不是数据本身:
// ❌ 错误做法:直接传递大对象
@Serializable data class ArticleScreen(val article: Article) // 太大!
// ✅ 正确做法:只传 ID,目标页面自己加载
@Serializable data class ArticleScreen(val articleId: String)
// 目标页面
@Composable
fun ArticleView(articleId: String) {
val article by viewModel.articleById(articleId).collectAsStateWithLifecycle()
// ...
}
三、深层链接 (DeepLink):外部跳转
DeepLink 是从外部(通知、网页、其他 App)跳转到 App 指定页面的机制。
3.1 配置 DeepLink
@Composable
fun AppNavHost() {
NavHost(navController = navController, startDestination = Screen.Home) {
composable<Screen.Article>(
deepLinks = listOf(
navDeepLink {
uriPattern = "https://example.com/article/{id}"
action = Intent.ACTION_VIEW
}
)
) { backStackEntry ->
val article: Screen.Article = backStackEntry.toRoute()
ArticleScreen(article.id)
}
}
}
对应的 AndroidManifest.xml 配置:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- DeepLink Activity 配置 -->
<intent-filter android:autoVerify="true">
<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="example.com"
android:pathPrefix="/article" />
</intent-filter>
</activity>
3.2 通知跳转 DeepLink
// 创建 PendingIntent
fun createDeepLinkIntent(context: Context, articleId: String): PendingIntent {
val deepLink = "https://example.com/article/$articleId"
val intent = Intent(Intent.ACTION_VIEW, deepLink.toUri())
return TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}
四、导航动画
Navigation Compose 2.8 强化了动画支持,可以在导航时自定义进出动画。
composable<Screen.Article>(
enterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(300))
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { -fullWidth / 3 },
animationSpec = tween(300)
) + fadeOut(animationSpec = tween(300))
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { -fullWidth / 3 },
animationSpec = tween(300)
) + fadeIn(animationSpec = tween(300))
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth },
animationSpec = tween(300)
) + fadeOut(animationSpec = tween(300))
}
)
五、常见陷阱与最佳实践
5.1 回到上一页的正确方式
使用 navController.popBackStack() 而不是 simulate 返回键。如果配合 DeepLink,使用 navigateUp() 更安全:
// 安全的返回
fun NavController.safePopBackStack(): Boolean {
return if (previousBackStackEntry != null) {
popBackStack()
} else {
// 没有上一页时退出 App
finish()
}
}
// navigateUp 自动处理 BackStack
fun NavController.safeNavigateUp(): Boolean {
return navigateUp()
}
5.2 避免重复导航
用户在快速点击按钮时,可能会触发多次导航,导致同一页面被多次压入栈:
// 在 NavController 中防止重复导航
fun NavController.navigateOnce(destination: Any) {
val currentRoute = currentDestination?.route
val targetRoute = destination.toString() // 简化示意
if (currentRoute != targetRoute) {
navigate(destination)
}
}
// 或者更可靠的方案:基于 NavBackStackEntry ID
fun NavController.navigateSafe(destination: Any) {
currentBackStackEntry?.lifecycle?.let { lifecycle ->
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
navigate(destination)
}
}
}
5.3 导航结果回调
在目标页面操作完成后,将结果回传前一个页面:
// 当前页面
@Composable
fun EditorScreen(onResult: (String) -> Unit) {
Button(onClick = {
// 保存成功后回传结果
val savedResult = viewModel.save()
savedResult?.let { onResult(it) }
}) {
Text("保存")
}
}
// NavHost 中注册
composable<Screen.Editor> { backStackEntry ->
val savedStateHandle = backStackEntry.savedStateHandle
EditorScreen(
onResult = { result ->
savedStateHandle["result"] = result
navController.previousBackStackEntry?.savedStateHandle?.set("result", result)
navController.popBackStack()
}
)
}
// 返回时读取
composable<Screen.Home> {
val savedStateHandle = it.savedStateHandle
val result = savedStateHandle.get<String>("result")
LaunchedEffect(result) {
result?.let {
println("Editor returned: $it")
savedStateHandle.remove<String>("result")
}
}
}
六、总结
Compose Navigation 核心要点:
- 类型安全导航:用 @Serializable 密封类替代字符串路由,编译期保证安全
- 多模块解耦:通过接口定义导航依赖,保持模块间的单向依赖
- DeepLink:统一处理外部跳转和通知推送,加深用户触达
- 动画:合理的转场动画提升用户体验,但不要过度
- 安全防护:防止重复导航、处理空 BackStack
Navigation Compose 2.8+ 的类型安全导航是一个里程碑式的更新,强烈建议新项目直接使用。它不仅让代码更安全,也大幅提升了开发体验和工程可维护性。