34、Navigation组件:NavHost、NavController、Safe Args、深层链接
Navigation 组件,说白了就是帮我们管理 Fragment 跳转的那套东西。以前我们写 Fragment 跳转,要么用 FragmentManager 手动 add/replace,要么用第三方的 Router 框架。Google 后来看不下去了,推出了官方的 Navigation 组件。我个人觉得,这套东西最大的好处就是——可视化。你可以在 AS 里直接看到页面之间的流转关系,像画流程图一样。
嗯,咱们今天就把 NavHost、NavController、Safe Args 和深层链接这四个核心概念捋一遍。我会结合我实际项目中的踩坑经历来讲,希望能帮你少走弯路。
NavHost:页面的容器
NavHost 说白了就是一个容器,用来承载你定义的各个页面(Destination)。在 Jetpack Compose 里,我们通常用 NavHost 这个 Composable 函数;在传统 View 体系里,用的是 NavHostFragment。
它的作用很简单:根据当前的导航状态,显示对应的页面。你想想看,如果没有 NavHost,那 Fragment 的切换就得你自己写一堆逻辑,还要处理返回栈。有了它,你只需要定义好导航图(NavGraph),剩下的交给框架。
核心要点:一个 Activity 里通常只有一个 NavHost,它管理着所有 Fragment 或 Composable 的生命周期和返回栈。
我在项目中遇到过一个问题:一个 Activity 里放了两个 NavHost,分别对应底部导航栏的两个 Tab。结果返回栈乱成一锅粥,用户按返回键时经常跳到不该去的页面。后来我改成单 NavHost + 嵌套导航图(Nested NavGraph)才解决。所以我的建议是:尽量保持一个 Activity 只有一个 NavHost,除非你有非常特殊的需求。
NavController:导航的指挥官
NavController 是 Navigation 组件的核心。它负责执行导航动作、管理返回栈、处理深层链接等。你可以通过 NavHostController(Compose 中)或 NavController(View 体系中)来获取它的实例。
常用的方法就几个:
navigate(destinationId):跳转到指定页面navigate(route):通过路由字符串跳转(Compose 中常用)popBackStack():返回上一个页面currentBackStackEntry:获取当前栈顶的页面信息
举个例子,在 Compose 中获取 NavController 并跳转:
@Composable
fun MyScreen(navController: NavHostController) {
Button(onClick = {
navController.navigate("detail/123")
}) {
Text("跳转到详情页")
}
}
小技巧:我习惯把 NavController 放在顶层 Composable 中,通过参数传递给子页面。这样每个子页面不需要自己创建 NavController,也方便做单元测试。
Safe Args:类型安全的参数传递
以前我们传参数,要么用 Bundle,要么用 arguments。这种方式有个问题:类型不安全。你传了个 Int,接收方却当成 String 来取,运行时直接崩溃。Safe Args 就是来解决这个问题的。
它通过生成一个专门的类(比如 DetailFragmentArgs),让你在编译期就能检查参数类型是否正确。说白了,就是把运行时错误提前到编译期发现。
使用步骤很简单:
- 在导航图(nav_graph.xml)中定义参数
- 在目标 Fragment 中通过
DetailFragmentArgs.fromBundle(arguments)获取参数
举个例子,导航图中定义:
<fragment
android:id="@+id/detailFragment"
android:name="com.example.DetailFragment">
<argument
android:name="itemId"
app:argType="integer"
android:defaultValue="0" />
</fragment>
然后在 DetailFragment 中获取:
val args: DetailFragmentArgs = DetailFragmentArgs.fromBundle(requireArguments())
val itemId = args.itemId
注意:我曾经犯过一个错误——在导航图中定义了参数,但忘记在 Fragment 中调用 fromBundle,而是直接用了 arguments?.getInt("itemId")。结果参数名拼写错了,查了半天才发现。所以我的建议是:既然用了 Safe Args,就全程用它,不要混用。
深层链接:从外部打开指定页面
深层链接(Deep Link)允许你从外部(比如通知、浏览器、其他 App)直接打开 App 中的某个页面。Navigation 组件对深层链接的支持非常友好,你只需要在导航图中配置 <deepLink> 标签即可。
配置方式:
<fragment
android:id="@+id/detailFragment"
android:name="com.example.DetailFragment">
<deepLink app:uri="myapp://detail/{itemId}" />
</fragment>
然后在 AndroidManifest.xml 中为 Activity 添加 intent-filter:
<activity
android:name=".MainActivity"
android:launchMode="singleTask">
<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>
这样,当用户点击 myapp://detail/456 这个链接时,App 就会自动打开 DetailFragment,并且 itemId 参数会被自动赋值为 456。
避坑指南:我曾经在配置深层链接时,忘了在 Activity 的 onNewIntent() 中处理新的 Intent。结果用户从外部链接打开 App 后,页面没有刷新,还是显示之前的页面。正确的做法是:在 onNewIntent() 中调用 navController.handleDeepLink(intent)。
知识体系总览
下面这张图展示了 Navigation 组件的核心逻辑和它们之间的关系:
实际项目中的最佳实践
说了这么多理论,咱们来点实际的。我在一个电商项目中,用 Navigation 组件实现了完整的页面跳转体系。这里分享几个经验:
- 统一管理路由:把所有页面的路由定义在一个常量类中,避免硬编码字符串。比如
const val ROUTE_DETAIL = "detail/{itemId}"。 - 使用 sealed class 定义导航动作:这样可以在编译期检查导航目标是否存在,避免运行时崩溃。
- 返回栈管理:在跳转时,如果不需要保留当前页面,可以调用
navController.navigate(route) { popUpTo(起始页面) { inclusive = true } },这样用户按返回键时不会回到不该去的页面。
总结一下:NavHost 是容器,NavController 是执行者,Safe Args 是安全员,深层链接是外部入口。这四个东西配合起来,基本能覆盖 App 中 90% 的导航需求。剩下的 10%,比如复杂的动画、跨模块导航,就需要你结合实际情况灵活处理了。
嗯,今天就聊到这儿。Navigation 组件其实还有很多细节,比如嵌套导航图、导航动画、测试等。但核心的四个概念你掌握了,日常开发基本够用。剩下的,遇到具体问题再查文档也不迟。