详细且通俗易懂地拆解 Android Navigation 组件。想象一下你在开发一个 App,里面有多个页面(通常是 Fragment),用户需要在这些页面之间跳转(比如从首页点击进入商品详情,再点击进入购物车)。Navigation 就是 Google 官方提供的帮你优雅、安全、可视化地管理这些页面跳转关系的工具箱。
核心目标: 简化 Fragment(或 Activity/Dialog/自定义目标)之间的导航逻辑,处理返回栈,传递参数,并支持深度链接。
一、如何使用(手把手入门)
想象你要做一个简单的 App:HomeFragment
-> 点击按钮 -> DetailFragment
-
添加依赖 (build.gradle):
kotlindependencies { def nav_version = "2.7.7" // 使用当前最新稳定版 implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" }
-
创建导航图 (Navigation Graph -
nav_graph.xml
):-
在
res/navigation/
文件夹下新建一个 XML 文件(如nav_graph.xml
)。 -
这个文件就是你的 "地图" ,定义了所有可以到达的"地点"(
Fragment
等,称为 目标/Destination )以及连接这些地点的"路线"(动作/Action)。 -
在可视化编辑器(Design 视图)或 XML 代码中:
xml<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph" app:startDestination="@id/homeFragment"> <!-- 指定入口页面 --> <!-- 定义目标1:首页 --> <fragment android:id="@+id/homeFragment" android:name="com.example.app.HomeFragment" android:label="Home" tools:layout="@layout/fragment_home"> <!-- 定义从homeFragment到detailFragment的动作 --> <action android:id="@+id/action_home_to_detail" app:destination="@id/detailFragment" /> </fragment> <!-- 定义目标2:详情页 --> <fragment android:id="@+id/detailFragment" android:name="com.example.app.DetailFragment" android:label="Detail" tools:layout="@layout/fragment_detail" /> </navigation>
-
-
设置导航宿主 (NavHostFragment):
- 在你的主 Activity 的布局文件 (
activity_main.xml
) 中,放置一个 "容器" (NavHostFragment
)。 - 这个容器告诉 Navigation 组件:"在这个区域里显示的 Fragment,都由我来管理它们的进出和切换"。
xml<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 关键!NavHostFragment容器 --> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" <!-- 指定实现类是NavHostFragment --> android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" <!-- 很重要!让这个NavHost处理系统返回键 --> app:navGraph="@navigation/nav_graph" /> <!-- 关联之前定义的导航图 --> </FrameLayout>
- 在你的主 Activity 的布局文件 (
-
执行导航 (使用 NavController):
-
在
HomeFragment
中,当按钮被点击时,你需要找到 "导航仪" (NavController
)。 -
这个导航仪知道当前在哪个位置 (
NavHostFragment
),也有一张地图 (nav_graph.xml
)。你告诉它:"执行action_home_to_detail
这个动作"。 -
Kotlin 示例 (在 Fragment 中):
kotlinclass HomeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val navController = findNavController() // 获取绑定到当前视图的NavController binding.goToDetailButton.setOnClickListener { // 使用导航控制器执行指定的导航动作 navController.navigate(R.id.action_home_to_detail) } } }
-
效果: 点击按钮后,
NavHostFragment
容器中的内容会从HomeFragment
平滑地替换成DetailFragment
,并且系统会自动将HomeFragment
放入返回栈。按返回键会回到HomeFragment
。
-
-
传递参数 (Arguments):
-
在导航图中定义参数:
xml<fragment android:id="@+id/detailFragment" ... > <argument android:name="itemId" app:argType="integer" /> <!-- 定义需要接收一个整数参数 itemId --> </fragment>
-
在发起导航时传递参数:
scssbinding.goToDetailButton.setOnClickListener { // 创建一个 Bundle 或使用 Safe Args (推荐,见下面概念) val bundle = bundleOf("itemId" to 12345) navController.navigate(R.id.action_home_to_detail, bundle) }
-
在
DetailFragment
中获取参数:inival itemId = arguments?.getInt("itemId") ?: 0 // 或者使用 Safe Args
-
二、原理
-
核心三剑客:
-
NavGraph
(导航图): 你的"地图"。它本质上是一个数据结构(XML 被解析成对象),存储了所有 Destination (目标) 和 Action (动作) 的定义,以及起点。 -
NavHost
/NavHostFragment
(导航宿主): 你的"容器"。它是一个特殊的 Fragment (或 ViewGroup 的子类实现),负责在屏幕上提供一个区域,让 Navigation 组件动态地切换显示不同的 Destination(通常是 Fragment)。它持有NavController
。 -
NavController
(导航控制器): 你的"导航仪"和"驾驶员"。它是实际执行导航操作的核心类。它:- 知道当前在哪个 Destination (通过
NavBackStackEntry
)。 - 持有
NavGraph
(地图)。 - 知道如何根据
Action
的destinationId
找到目标 Destination。 - 管理一个返回栈 (
NavBackStack
) ,记录用户访问过的 Destination 历史。 - 协调
NavHost
进行实际的视图切换。
- 知道当前在哪个 Destination (通过
-
-
导航过程 (
navigate()
调用时):-
你调用
navController.navigate(actionId/bundle)
。 -
NavController
查找NavGraph
中对应的Action
。 -
NavController
根据Action
的destinationId
找到目标Destination
。 -
NavController
检查目标Destination
是否需要参数,并验证你传递的参数是否匹配(类型安全由Safe Args
插件在编译时保证更好)。 -
NavController
创建一个代表目标 Destination 状态的新NavBackStackEntry
(包含参数),并将其压入返回栈。 -
NavController
通知NavHost
:"嘿,容器,现在需要显示目标 Destination 了!" -
NavHost
(具体是NavHostFragment
) 使用FragmentManager
和FragmentTransaction
来执行实际的 Fragment 切换操作(如replace
,add
等,具体由Navigator
处理)。- 关键点: Navigation 内部针对不同类型的 Destination (
Fragment
,Activity
,Dialog
, 自定义) 使用了不同的Navigator
子类(如FragmentNavigator
,ActivityNavigator
)。NavController
委托给相应的Navigator
执行具体导航逻辑。Navigator
是真正干活的人(司机)。
- 关键点: Navigation 内部针对不同类型的 Destination (
-
切换动画(如果有定义在 Action 中)被执行。
-
目标 Fragment/视图显示在
NavHost
容器中。
-
-
返回栈管理:
-
每次通过
NavController.navigate()
导航到一个新目标(非弹出栈顶目标),该目标会被推入返回栈。 -
当用户点击系统返回键(或调用
navController.popBackStack()
):NavController
从返回栈弹出顶部的NavBackStackEntry
。- 通知
NavHost
恢复前一个 Destination 的状态。 NavHost
再次使用FragmentManager
/FragmentTransaction
切换回前一个 Fragment(通常是onCreateView
重新创建视图或恢复保存的状态)。
-
app:defaultNavHost="true"
让NavHostFragment
拦截系统返回键事件,并交由NavController.popBackStack()
处理,实现按预期返回。
-
三、源码调用链路
当你调用 navController.navigate(R.id.action_home_to_detail)
时,大致的调用链条如下:
-
NavController.navigate(@IdRes int resId, @Nullable Bundle args)
- 入口点。 -
NavController.navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras)
- 内部重载,处理参数和选项。 -
NavController.performNavigate(...)
- 核心导航方法。-
解析
resId
:如果是 Action ID,找到对应的NavAction
;如果是 Destination ID,创建一个指向该 ID 的虚拟 Action。 -
获取目标 Destination ID (
navAction.getDestinationId()
)。 -
创建目标
NavDestination
对象。 -
关键委托:
NavController.navigate(NavDestination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras)
-
根据
destination
的类型 (比如Fragment
),找到对应的Navigator
(比如FragmentNavigator
)。 -
Navigator.navigate(@NonNull D destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras)
- 调用具体 Navigator 的 navigate 方法。-
对于
FragmentNavigator.navigate(...)
:- 创建新的
Fragment
实例。 - 设置参数 (
args
)。 - 使用
FragmentManager
和FragmentTransaction
(ft
) 执行replace
(或add
+hide
/show
等策略) 操作,将新 Fragment 放入NavHostFragment
的容器中 (ft.replace(mContainerId, fragment)
). - 设置转场动画 (
navOptions
或默认)。 - 执行事务 (
ft.commit()
).
- 创建新的
-
-
创建新的
NavBackStackEntry
(包含目标 Destination 和参数) 并压入NavController
管理的返回栈 (mBackStack
)。
-
-
四、关键概念与使用场景
-
导航图 (
NavGraph
):- 概念: XML 文件,定义所有目的地和动作。可视化编辑器让结构一目了然。
- 场景: 任何需要多个页面跳转的 App。是 Navigation 的核心配置。
-
目的地 (
Destination
):- 概念: 导航图中的节点。可以是
Fragment
(最常用),Activity
,DialogFragment
, 甚至自定义视图 (<navigation>
元素本身可用于嵌套图)。 - 场景: 应用中的每一个主要屏幕或对话框。
- 概念: 导航图中的节点。可以是
-
动作 (
Action
):- 概念: 连接两个目的地的箭头。定义了从哪里 (
source
) 到哪里 (destination
),可以包含动画、弹出行为 (popUpTo
/popUpToInclusive
) 等配置。 - 场景: 定义页面跳转逻辑。例如 "登录成功 -> 跳转到主页并清空登录相关返回栈"。
- 概念: 连接两个目的地的箭头。定义了从哪里 (
-
导航宿主 (
NavHostFragment
):- 概念: 承载目的地切换的容器。通常是布局中的一个
FragmentContainerView
,指定了android:name="androidx.navigation.fragment.NavHostFragment"
。 - 场景: 主 Activity 的布局中必须包含它。是导航发生的舞台。
- 概念: 承载目的地切换的容器。通常是布局中的一个
-
导航控制器 (
NavController
):- 概念: 执行导航操作的引擎。绑定到
NavHostFragment
或Activity
。通过findNavController()
获取。 - 场景: 在代码中触发导航 (
navigate()
,popBackStack()
) 或监听导航事件。
- 概念: 执行导航操作的引擎。绑定到
-
返回栈 (
NavBackStack
):- 概念:
NavController
内部维护的栈,记录用户访问过的目的地历史 (NavBackStackEntry
列表)。 - 场景: 实现返回键逻辑。
popUpTo
动作可以控制栈的深度(如登录成功后清空所有登录相关页面)。
- 概念:
-
参数 (
Arguments
):- 概念: 传递给目标目的地的数据。在导航图中定义类型。
- 场景: 向下一个页面传递信息(如商品ID、用户信息)。推荐使用 Safe Args Gradle 插件 ,它在编译时生成类型安全的参数类和方法,避免运行时错误和
Bundle
键名拼写错误。
-
Safe Args:
-
概念: Gradle 插件,为导航图中的每个 Action 和 Destination 生成类型安全的参数类和方法。
-
场景: 强烈推荐所有项目使用! 安全、方便地传递和接收参数:
- 生成
Directions
类 (如HomeFragmentDirections.actionHomeToDetail(itemId = 123)
) 用于导航。 - 生成
Args
类 (如DetailFragmentArgs.fromBundle(arguments).itemId
) 用于获取参数。
- 生成
-
-
嵌套图 (
<navigation>
元素作为 Destination):- 概念: 将一组相关的目的地和动作封装在一个子导航图中。子图有自己的
startDestination
。 - 场景: 模块化导航逻辑。例如,将 "登录/注册流程"、"设置项"、"主内容区域" 分别封装成嵌套图。简化主图结构,提高复用性。
- 概念: 将一组相关的目的地和动作封装在一个子导航图中。子图有自己的
-
全局动作 (Global Action):
- 概念: 定义在根
<navigation>
元素下的 Action,可以从任何目的地导航到它的目标。 - 场景: 需要从多个不同入口到达同一个页面(如全局的 "帮助" 页面、"退出登录" 动作)。
- 概念: 定义在根
-
深度链接 (Deep Link):
-
概念: 在导航图中为 Destination 添加
<deepLink>
。允许 App 通过特定的 URI (或 Intent Action/MIME Type) 直接打开该 Destination,并可携带参数。 -
场景:
- 网页链接直接打开 App 内特定内容页。
- 通知点击跳转到特定页面。
- App 间跳转到特定功能。
- 桌面快捷方式。
-
-
导航与 UI 组件集成 (Navigation UI):
-
概念:
NavigationUI
类提供了便捷方法,将NavController
与常见的 Material Design UI 组件绑定:Toolbar
/ActionBar
: 自动更新标题,显示返回按钮并处理点击。BottomNavigationView
: 切换底部 Tab 时自动导航到对应 Destination,反之亦然。NavigationView
(抽屉菜单): 点击菜单项时自动导航。OnDestinationChangedListener
: 监听导航目的地变化,执行自定义 UI 更新。
-
场景: 快速实现具有标准导航模式(如底部导航+返回键)的应用。保持导航状态与 UI 同步。
-
总结与优势
- 可视化: 导航图让你清晰地看到 App 的页面结构和跳转关系。
- 标准化: 提供了一套统一、声明式的 API 管理导航逻辑,替代了分散在各处的
startActivity
和FragmentTransaction
。 - 简化: 大大减少了管理 Fragment 事务和返回栈的样板代码。
- 安全: Safe Args 插件提供了类型安全的参数传递。
- 强大: 支持嵌套图、全局动作、深度链接、与 UI 组件集成等高级功能。
- 生命周期感知: 与 Android 架构组件(如 ViewModel)结合良好,导航状态变化可被监听。
- 单 Activity 架构推动者: Navigation 是 Google 推荐的单 Activity 应用架构(多个 Fragment 在一个 Activity 中切换)的理想伴侣。
何时使用: 几乎所有包含多个 Fragment 的 Android App 都应该考虑使用 Navigation 组件来管理核心导航流程。它能显著提升开发效率和代码的可维护性、可读性。对于简单的只有 1-2 个 Fragment 的 App,手动管理可能更直接,但随着复杂度增加,Navigation 的优势会越来越明显。
建议结合 Google 的官方文档(使用 Navigation 组件) 动手实践,理解会更深刻!