导航与路由:Navigation Compose 入门
导航,说白了就是页面之间的跳转逻辑。在传统 Android 开发中,我们用 FragmentTransaction 或者 Intent 来切页面。但在 Jetpack Compose 里,一切都变了——没有 Fragment,没有 Activity 栈,取而代之的是 Navigation Compose。
我第一次接触 Navigation Compose 时,心里其实有点抵触。毕竟 Fragment 那套导航我用了好多年,突然换成声明式的方式,总觉得不踏实。但用了一个项目之后,我彻底改观了。嗯,真香。
核心三要素:NavHost、NavController、NavBackStackEntry。记住这三个,导航就通了。
NavController:导航的总指挥
NavController 是导航的核心对象。它管理着一个「返回栈」,记录了用户访问过的所有页面。你可以把它想象成一个遥控器——按一下,跳转;按返回键,回退。
获取 NavController 的方式很简单:
val navController = rememberNavController()
注意这个 rememberNavController()。它是个 Composable 函数,会记住 NavController 实例。如果你的 Activity 重建了,它也不会丢失状态。我在项目中遇到过一个问题:忘记用 remember,结果每次重组都新建一个 NavController,导航直接崩了。后来排查了半天,才发现是这行代码的问题。
NavHost:导航的容器
NavHost 是页面的容器。它接收 NavController 和一个起始路由,然后根据路由来展示对应的 Composable 页面。
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") { HomeScreen(navController) }
composable("profile") { ProfileScreen(navController) }
composable("settings") { SettingsScreen(navController) }
}
每个 composable("路由") 定义了一个页面。路由就是字符串,你可以随意命名。我个人习惯用下划线命名法,比如 user_detail、order_list,这样看起来清晰。
小技巧:路由字符串最好定义成常量,避免拼写错误。我一般会写一个 Routes 对象来统一管理。
页面跳转:navController.navigate()
跳转页面只需要调用 navController.navigate("路由")。比如从首页跳到个人中心:
Button(onClick = {
navController.navigate("profile")
}) {
Text("去个人中心")
}
这里有个细节:默认情况下,跳转后会把新页面压入返回栈。如果你不想让用户返回到上一个页面(比如登录成功后),可以加上 launchSingleTop = true 或者清除返回栈:
navController.navigate("home") {
popUpTo("login") { inclusive = true }
}
这段代码的意思是:跳转到 home,同时把 login 页面从栈里移除。用户按返回键就不会回到登录页了。我曾经在项目里没加这个,结果用户登录后按返回键又回到登录页,体验极差。后来加上了 popUpTo,问题解决。
参数传递:导航时带数据
页面之间传参,是导航的刚需。Navigation Compose 支持两种方式:路径参数和查询参数。
路径参数
路径参数就是把参数写在路由里,像 RESTful API 那样:
// 定义路由
composable(
route = "user/{userId}",
arguments = listOf(
navArgument("userId") { type = NavType.StringType }
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
UserDetailScreen(userId = userId)
}
// 跳转时传参
navController.navigate("user/12345")
路径参数适合必传的参数,比如用户 ID、订单号。如果参数是可选的,可以用查询参数。
查询参数
// 定义路由
composable(
route = "search?query={query}",
arguments = listOf(
navArgument("query") {
type = NavType.StringType
defaultValue = ""
}
)
) { backStackEntry ->
val query = backStackEntry.arguments?.getString("query")
SearchScreen(query = query)
}
// 跳转时传参
navController.navigate("search?query=Android")
查询参数的好处是可选,不传就用默认值。我个人更推荐路径参数,因为路由更清晰,不容易出错。
注意:参数类型要匹配。如果你声明了 NavType.IntType,传了字符串会直接崩溃。我建议所有参数都用 String 类型,然后在 Composable 里自己转换,这样更安全。
深层链接:从外部打开指定页面
深层链接(Deep Link)允许你从通知、浏览器或其他 App 直接打开 App 的某个页面。Navigation Compose 对深层链接的支持非常优雅。
composable(
route = "article/{articleId}",
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://article/{articleId}" }
)
) { backStackEntry ->
val articleId = backStackEntry.arguments?.getString("articleId")
ArticleDetailScreen(articleId = articleId)
}
配置好之后,用户在浏览器点击 myapp://article/888,就会直接打开你的文章详情页。注意,你还需要在 AndroidManifest.xml 中声明 intent-filter:
<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="myapp" />
</intent-filter>
</activity>
深层链接在项目里用得不多,但一旦需要,它就是救命稻草。我记得有一次产品经理要求从推送通知直接跳转到订单详情页,用深层链接几分钟就搞定了。
知识体系图
下面这张图总结了 Navigation Compose 的核心逻辑:
常见问题与避坑指南
- 页面重复创建:多次调用
navigate("home")会创建多个 HomeScreen 实例。解决方案是使用launchSingleTop = true。 - 参数丢失:Activity 被系统回收后,NavController 的状态会恢复,但参数不会。建议用 SavedStateHandle 来保存关键参数。
- 深层链接不生效:检查 AndroidManifest.xml 中的 intent-filter 是否配置正确。我曾经漏写了
BROWSABLE类别,折腾了一下午。 - 返回栈混乱:跳转时忘记
popUpTo,导致用户按返回键回到不该回的页面。建议在登录、支付等关键流程中,显式管理返回栈。
我的个人习惯:每个页面的路由都定义成常量,放在一个 NavRoutes.kt 文件里。参数 key 也定义成常量。这样团队协作时,大家不会因为拼写错误而吵架。
Navigation Compose 其实不复杂。你只要记住:NavController 是遥控器,NavHost 是电视屏幕,路由表是频道列表。剩下的就是多写多练,踩过几个坑之后,自然就熟练了。