嵌套导航图:为什么需要嵌套?

说实话,我第一次接触嵌套导航图时,心里想的是:「这不就是多套了一层壳吗?」

后来在做一个电商项目时,我彻底改变了这个看法。那个项目有首页、分类、购物车、个人中心四个主模块,每个模块下面又有三到四层子页面。如果全部平铺在一个导航图里……嗯,那画面太美我不敢看。

嵌套导航图,说白了就是把导航图再拆成子图。每个子图管理自己那一亩三分地的页面跳转,父图只负责调度子图。这样代码结构清晰,职责分明,维护起来也舒服得多。

为什么需要嵌套?

我总结下来,主要有三个场景必须用嵌套:

  • 模块化开发:每个功能模块独立成子图,团队并行开发互不干扰
  • 独立返回栈:子图内部有自己的返回栈,不会和父图混在一起
  • 深层链接隔离:不同模块的深层链接各自处理,不会串路

核心思想:嵌套导航图 = 页面分组 + 独立返回栈 + 模块化边界

举个例子。我在做那个电商项目时,购物车模块有「购物车列表 → 结算页 → 支付页 → 支付结果页」这一串流程。如果不嵌套,这些页面会全部塞进主导航图里,返回逻辑乱成一锅粥。嵌套之后,购物车子图自己管自己的返回栈,父图只需要知道「跳转到购物车模块」这一个入口就够了。

创建嵌套图

创建嵌套图有两种方式,我习惯用 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)

这样跳转到购物车模块后,按返回键会回到首页,而不是退出整个应用。

嵌套图的知识体系

我把这一章的核心逻辑画成了图,方便你理解:

父导航图 (nav_graph_main) homeFragment startDestination 子图 (cart_graph) cartListFragment startDestination checkoutFragment paymentFragment paymentResultFragment navigate 返回栈:首页 → 购物车列表 → 结算页 → 支付页 按返回键:支付页 → 结算页 → 购物车列表 → 首页

从图上可以清楚看到:父图只管理 homeFragment 和子图入口,子图内部的所有页面跳转和返回栈都是自治的。这就是嵌套导航的核心魅力——各管各的,互不干扰

一些实用建议

最后分享几个我在实战中总结的经验:

  • 子图不宜过深:我建议子图内部最多三层页面,超过三层用户容易迷失方向
  • 子图之间不要互相跳转:如果购物车要跳转到个人中心,应该通过父图来调度,而不是直接跨子图跳转
  • 善用 popUpTo:在子图内部做跳转时,用 popUpTo 清理不必要的中间页面,保持返回栈干净
  • 测试返回栈行为:每次修改导航图后,手动走一遍所有返回路径,确保符合预期

一个小技巧:在子图的 startDestination 的 onViewCreated 里加个日志,打印当前返回栈的路径。调试时特别有用:

val navController = findNavController()
Log.d("CartGraph", "当前返回栈: ${navController.backStack}")

嵌套导航图用好了,你的项目结构会非常清爽。每个模块独立开发、独立测试、独立维护。团队协作时,A 组改购物车模块,B 组改个人中心模块,导航图层面完全不会冲突。

嗯,这就是嵌套导航的核心价值。下一节我们聊聊深层链接,那又是一个有意思的话题。


公众号:蓝海资料掘金营,微信deep3321