15、导航组件:Navigation Safe Args、深层链接与导航图设计
导航组件,说白了就是帮我们管理 Fragment 跳转的那一套东西。以前我们写 Fragment 跳转,要么用 FragmentManager 手动操作,要么用第三方的库。Google 推出 Navigation 组件后,我个人的项目基本都迁移过来了。为什么?因为它把跳转逻辑、参数传递、动画、甚至深层链接都统一管理了。
今天我们就来聊聊三个核心点:Safe Args 的类型安全、深层链接的配置、以及导航图的设计思路。
15.1 导航图设计:从一张图开始
导航图(NavGraph)是 Navigation 组件的核心。它用 XML 描述了你应用里所有页面的跳转关系。我个人习惯先画一张图,再写代码。你想想看,如果连页面之间的流转关系都没想清楚,代码写起来肯定乱。
导航图的核心要素:
- 目的地(Destination):每个 Fragment 或 Activity 都是一个目的地
- 动作(Action):从一个目的地到另一个目的地的跳转
- 参数(Argument):跳转时携带的数据
下面是我画的一张典型导航图,展示了一个电商 App 的页面流转关系:
这张图里,每个页面都是一个目的地,箭头表示跳转动作。你会发现有些跳转需要携带参数,比如从商品列表跳到详情页,必须带上商品 ID。这就是 Safe Args 要解决的问题。
15.2 Safe Args:告别手写 Bundle
以前我们传参数怎么写?
// 传统方式 - 容易出错
val bundle = Bundle()
bundle.putString("productId", "123")
bundle.putInt("quantity", 1)
findNavController().navigate(R.id.productDetailFragment, bundle)
这种方式的问题很明显:key 是字符串,拼错了编译器不会报错。类型也不安全,你传了个 Int 但接收方以为是 String,运行时直接崩溃。
Safe Args 就是来解决这个问题的。它会根据导航图自动生成类型安全的类。我在项目中用过之后,再也不想回到手写 Bundle 的日子了。
15.2.1 配置 Safe Args
首先在项目的 build.gradle 里加上插件:
// 根目录 build.gradle
buildscript {
repositories {
google()
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7"
}
}
// app 模块 build.gradle
plugins {
id 'androidx.navigation.safeargs.kotlin'
}
小提示:如果你用的是 Kotlin,记得用 safeargs.kotlin 而不是 safeargs。后者生成的是 Java 代码,用起来没那么舒服。
15.2.2 在导航图中定义参数
在 nav_graph.xml 里,给目的地加上 argument 标签:
<fragment
android:id="@+id/productDetailFragment"
android:name="com.example.app.ui.detail.ProductDetailFragment"
android:label="商品详情">
<argument
android:name="productId"
app:argType="string"
android:defaultValue="" />
<argument
android:name="quantity"
app:argType="integer"
android:defaultValue="1" />
<argument
android:name="isFromCart"
app:argType="boolean"
android:defaultValue="false" />
<deepLink
app:uri="myapp://product/{productId}" />
</fragment>
定义好之后,重新 build 一下项目。Safe Args 会自动生成几个类:
- ProductDetailFragmentArgs:包含所有参数的 data class
- ProductDetailFragmentDirections:包含所有跳转动作的类
15.2.3 使用生成的类
发送方:
// 使用 Safe Args 生成的 Directions 类
val action = ProductListFragmentDirections
.actionProductListFragmentToProductDetailFragment(
productId = "P10086",
quantity = 2,
isFromCart = false
)
findNavController().navigate(action)
接收方:
class ProductDetailFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 使用 Safe Args 生成的 Args 类
val args: ProductDetailFragmentArgs by navArgs()
Log.d("Detail", "productId: ${args.productId}")
Log.d("Detail", "quantity: ${args.quantity}")
Log.d("Detail", "isFromCart: ${args.isFromCart}")
}
}
关键好处:
- 编译时类型检查:参数类型错了直接编译失败
- 自动生成代码:不用手写 Bundle 的 put/get
- 支持默认值:没传参时使用默认值,不会崩溃
15.3 深层链接:从外部跳转到指定页面
深层链接(Deep Link)允许用户从外部——比如通知栏、浏览器、其他 App——直接跳转到你 App 的某个页面。我在项目中遇到过产品经理要求「从推送通知点进去直接到订单详情页」,这就是典型的深层链接场景。
15.3.1 配置深层链接
有两种方式配置深层链接:
方式一:在导航图中配置(推荐)
<fragment
android:id="@+id/orderDetailFragment"
android:name="com.example.app.ui.order.OrderDetailFragment">
<argument
android:name="orderId"
app:argType="string" />
<deepLink
app:uri="myapp://order/{orderId}" />
<deepLink
app:uri="https://www.example.com/order/{orderId}" />
</fragment>
方式二:在 AndroidManifest 中配置
<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"
android:host="order" />
</intent-filter>
</activity>
注意:Activity 的 launchMode 建议设为 singleTask。否则每次深层链接都会创建一个新的 Activity 实例,导致页面栈混乱。我曾经踩过这个坑,用户从通知点进来,按返回键却回到了桌面,而不是 App 的上一个页面。
15.3.2 处理深层链接的参数
深层链接中的占位符(如 {orderId})会自动映射到同名的 argument 上。在 Fragment 中获取参数的方式和 Safe Args 完全一样:
class OrderDetailFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val args: OrderDetailFragmentArgs by navArgs()
val orderId = args.orderId
// 根据 orderId 加载订单数据
loadOrder(orderId)
}
}
测试深层链接可以用 adb 命令:
# 测试自定义 scheme
adb shell am start -W -a android.intent.action.VIEW -d "myapp://order/ORD20240101"
# 测试 https 链接
adb shell am start -W -a android.intent.action.VIEW -d "https://www.example.com/order/ORD20240101"
15.4 导航图设计的最佳实践
导航图设计得好不好,直接影响项目的可维护性。我总结了几条经验:
| 实践 | 说明 | 我的建议 |
|---|---|---|
| 按功能模块拆分导航图 | 不要把所有页面放在一个 nav_graph.xml 里 | 每个模块一个导航图,用 include 引入 |
| 避免深层嵌套 | 导航图嵌套超过 3 层会难以维护 | 用全局动作代替深层嵌套 |
| 合理使用全局动作 | 全局动作可以从任何页面跳转到目标页面 | 只用于「返回首页」「跳转到登录」等场景 |
| 参数尽量少 | 每个目的地的参数不要超过 5 个 | 超过 5 个考虑用 ViewModel 共享数据 |
| 深层链接统一管理 | 所有深层链接的 URI 模式集中定义 | 建议用常量类维护,避免硬编码 |
15.4.1 模块化导航图示例
假设你的 App 有首页、商品、订单三个模块,可以这样组织:
res/navigation/
├── nav_main.xml # 主导航图,包含底部导航栏的页面
├── nav_product.xml # 商品模块的导航图
├── nav_order.xml # 订单模块的导航图
└── nav_user.xml # 用户模块的导航图
在主导航图中用 <include> 引入子导航图:
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_main"
app:startDestination="@id/homeFragment">
<include app:graph="@navigation/nav_product" />
<include app:graph="@navigation/nav_order" />
<include app:graph="@navigation/nav_user" />
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.ui.home.HomeFragment" />
</navigation>
小技巧:子导航图的 startDestination 不要直接暴露给用户。比如 nav_product.xml 的 startDestination 可以是一个空壳 Fragment,然后立即重定向到真正的商品列表页。这样可以避免用户通过深层链接直接进入子导航图的中间页面。
15.5 避坑指南
最后分享几个我踩过的坑:
- 参数类型不匹配:Safe Args 虽然类型安全,但如果你在导航图中改了参数类型,记得重新 build。否则生成的类还是旧的,运行时可能报错。
- 深层链接的 URI 冲突:多个目的地使用了相同的 URI 模式,Navigation 会随机选择一个。我曾经因为这个原因,用户从通知点进去跳到了错误的页面。解决办法是确保每个深层链接的 URI 模式唯一。
- Fragment 重建时参数丢失:如果 Fragment 被系统重建,通过 navArgs() 获取的参数仍然有效。但如果你在 ViewModel 中缓存了这些参数,记得用 SavedStateHandle 保存。
- 导航动画导致的内存泄漏:复杂的导航动画可能导致 Fragment 无法及时释放。建议动画时长不要超过 300ms,并且避免在动画期间执行耗时操作。
嗯,导航组件的内容差不多就这些了。Safe Args 让参数传递变得安全可靠,深层链接让外部跳转变得简单统一,而导航图设计则是整个架构的骨架。把这三点掌握好,你的页面跳转逻辑会清晰很多。
公众号:蓝海资料掘金营,微信deep3321