15、导航组件:Navigation Safe Args、深层链接与导航图设计

导航组件,说白了就是帮我们管理 Fragment 跳转的那一套东西。以前我们写 Fragment 跳转,要么用 FragmentManager 手动操作,要么用第三方的库。Google 推出 Navigation 组件后,我个人的项目基本都迁移过来了。为什么?因为它把跳转逻辑、参数传递、动画、甚至深层链接都统一管理了。

今天我们就来聊聊三个核心点:Safe Args 的类型安全、深层链接的配置、以及导航图的设计思路。

15.1 导航图设计:从一张图开始

导航图(NavGraph)是 Navigation 组件的核心。它用 XML 描述了你应用里所有页面的跳转关系。我个人习惯先画一张图,再写代码。你想想看,如果连页面之间的流转关系都没想清楚,代码写起来肯定乱。

导航图的核心要素:

  • 目的地(Destination):每个 Fragment 或 Activity 都是一个目的地
  • 动作(Action):从一个目的地到另一个目的地的跳转
  • 参数(Argument):跳转时携带的数据

下面是我画的一张典型导航图,展示了一个电商 App 的页面流转关系:

电商 App 导航图示例 商品列表页 ProductListFragment 商品详情页 ProductDetailFragment 购物车页 CartFragment 登录页 LoginFragment 订单确认页 OrderConfirmFragment 点击商品 加入购物车 需要登录 未登录 登录成功 已登录

这张图里,每个页面都是一个目的地,箭头表示跳转动作。你会发现有些跳转需要携带参数,比如从商品列表跳到详情页,必须带上商品 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