嵌套导航图:为什么需要嵌套?
说实话,我第一次接触嵌套导航图时,心里想的是:「这不就是多套了一层壳吗?」
后来在做一个电商项目时,我彻底改变了这个看法。那个项目有首页、分类、购物车、个人中心四个主模块,每个模块下面又有三到四层子页面。如果全部平铺在一个导航图里……嗯,那画面太美我不敢看。
嵌套导航图,说白了就是把导航图再拆成子图。每个子图管理自己那一亩三分地的页面跳转,父图只负责调度子图。这样代码结构清晰,职责分明,维护起来也舒服得多。
为什么需要嵌套?
我总结下来,主要有三个场景必须用嵌套:
- 模块化开发:每个功能模块独立成子图,团队并行开发互不干扰
- 独立返回栈:子图内部有自己的返回栈,不会和父图混在一起
- 深层链接隔离:不同模块的深层链接各自处理,不会串路
核心思想:嵌套导航图 = 页面分组 + 独立返回栈 + 模块化边界
举个例子。我在做那个电商项目时,购物车模块有「购物车列表 → 结算页 → 支付页 → 支付结果页」这一串流程。如果不嵌套,这些页面会全部塞进主导航图里,返回逻辑乱成一锅粥。嵌套之后,购物车子图自己管自己的返回栈,父图只需要知道「跳转到购物车模块」这一个入口就够了。
创建嵌套图
创建嵌套图有两种方式,我习惯用 XML 方式,因为直观。先看代码:
<!-- nav_graph_main.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph_main"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name=".HomeFragment" />
<!-- 嵌套子图 -->
<navigation
android:id="@+id/cart_graph"
app:startDestination="@id/cartListFragment"
android:label="购物车模块">
<fragment
android:id="@+id/cartListFragment"
android:name=".CartListFragment" />
<fragment
android:id="@+id/checkoutFragment"
android:name=".CheckoutFragment" />
<fragment
android:id="@+id/paymentFragment"
android:name=".PaymentFragment" />
<fragment
android:id="@+id/paymentResultFragment"
android:name=".PaymentResultFragment" />
</navigation>
</navigation>
你看,<navigation> 标签里面再套一个 <navigation>,这就是嵌套。子图有自己的 startDestination,也有自己的 id。父图根本不需要知道子图内部有哪些页面,它只知道「有个叫 cart_graph 的子图」就够了。
我的习惯:子图的 id 用下划线命名法,比如 cart_graph、profile_graph,一眼就能看出是子图。Fragment 的 id 用驼峰,比如 cartListFragment,区分清楚。
如果你用 Navigation Editor 可视化编辑器,操作更简单:右键 → New → Nested Graph,然后拖 Fragment 进去就行。但我个人还是推荐手写 XML,因为你能完全掌控每个细节。
从父图跳转到子图
跳转到子图,其实就是跳转到子图的 startDestination。比如从首页跳转到购物车列表:
// 在 HomeFragment 中
findNavController().navigate(R.id.cart_graph)
就这么简单。你传的是子图的 id,Navigation 会自动找到子图的 startDestination(也就是 cartListFragment)并打开它。
如果你想跳转到子图内部的某个特定页面,比如直接跳到支付页:
// 直接跳转到子图内部的 paymentFragment
findNavController().navigate(R.id.paymentFragment)
这里要注意:直接跳转子图内部页面时,返回栈的行为会不一样。我踩过这个坑,后面会细说。
传参也很自然,和普通跳转一样:
val bundle = Bundle().apply {
putString("productId", "12345")
putDouble("price", 299.0)
}
findNavController().navigate(R.id.cart_graph, bundle)
子图的 startDestination 在 onViewCreated 里通过 arguments 就能拿到这些参数。
管理嵌套图的返回栈
这是嵌套导航最核心、也最容易出问题的地方。我直接说结论:
- 子图内部跳转:返回栈在子图内部管理,按正常顺序回退
- 从父图跳入子图:子图作为一个整体入栈,按返回键时先走完子图内部栈,再回到父图
- 直接跳转子图内部页面:返回栈可能不会按你预期的方式工作
举个例子。假设当前在首页(父图),然后跳转到购物车列表(子图 startDestination),再跳转到结算页,再跳转到支付页。这时候返回栈是这样的:
首页 → 购物车列表 → 结算页 → 支付页
按返回键:支付页 → 结算页 → 购物车列表 → 首页。完全符合直觉。
但如果你从首页直接跳转到支付页(跳过购物车列表和结算页),返回栈是这样的:
首页 → 支付页
按返回键就直接回到首页了。购物车列表和结算页根本没进栈。这有时候是你要的效果,有时候不是。我建议你明确业务场景再决定用哪种跳法。
我曾经踩过的坑:在一个订单流程中,我从外部通知直接跳转到支付页,用户支付完成后按返回键,直接跳出了购物车模块,回到了首页。用户一脸懵:「我刚付完钱怎么回首页了?」后来我改成先跳转到购物车列表,再自动跳转到支付页,返回栈就正常了。
如果你想手动控制返回栈,可以用 NavOptions:
val navOptions = NavOptions.Builder()
.setPopUpTo(R.id.homeFragment, inclusive = false) // 回到首页但不移除首页
.setLaunchSingleTop(true)
.build()
findNavController().navigate(R.id.cart_graph, null, navOptions)
这样跳转到购物车模块后,按返回键会回到首页,而不是退出整个应用。
嵌套图的知识体系
我把这一章的核心逻辑画成了图,方便你理解:
从图上可以清楚看到:父图只管理 homeFragment 和子图入口,子图内部的所有页面跳转和返回栈都是自治的。这就是嵌套导航的核心魅力——各管各的,互不干扰。
一些实用建议
最后分享几个我在实战中总结的经验:
- 子图不宜过深:我建议子图内部最多三层页面,超过三层用户容易迷失方向
- 子图之间不要互相跳转:如果购物车要跳转到个人中心,应该通过父图来调度,而不是直接跨子图跳转
- 善用
popUpTo:在子图内部做跳转时,用popUpTo清理不必要的中间页面,保持返回栈干净 - 测试返回栈行为:每次修改导航图后,手动走一遍所有返回路径,确保符合预期
一个小技巧:在子图的 startDestination 的 onViewCreated 里加个日志,打印当前返回栈的路径。调试时特别有用:
val navController = findNavController()
Log.d("CartGraph", "当前返回栈: ${navController.backStack}")
嵌套导航图用好了,你的项目结构会非常清爽。每个模块独立开发、独立测试、独立维护。团队协作时,A 组改购物车模块,B 组改个人中心模块,导航图层面完全不会冲突。
嗯,这就是嵌套导航的核心价值。下一节我们聊聊深层链接,那又是一个有意思的话题。
公众号:蓝海资料掘金营,微信deep3321