一、Navigation 组件:导航图设计、Safe Args 类型安全传参、深层链接与 BottomNavigation 联动

Navigation 组件,说白了就是 Google 给 Android 开发者准备的一套「导航框架」。我刚开始接触它的时候,心里还嘀咕:不就是页面跳转吗,用 Intent 不就行了?后来在项目里被各种 Fragment 事务、返回栈管理折磨得够呛,才意识到——嗯,这玩意儿真香。

今天咱们就把它掰开揉碎,从导航图设计到 Safe Args 传参,再到 DeepLink 和 BottomNavigation 联动,一条龙讲清楚。

1. 导航图(NavGraph)设计:你的页面地图

导航图,说白了就是一张「页面关系图」。你想想看,一个 App 少说十几个页面,它们之间怎么跳转、怎么返回、参数怎么传——这些逻辑如果散落在代码里,维护起来就是噩梦。

我个人习惯,在 res/navigation/ 目录下创建一个 nav_graph.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">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.HomeFragment"
        android:label="首页"
        tools:layout="@layout/fragment_home">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment" />
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.DetailFragment"
        android:label="详情"
        tools:layout="@layout/fragment_detail">
        <argument
            android:name="itemId"
            app:argType="integer" />
    </fragment>

</navigation>

这里有个小细节:startDestination 决定了 App 启动时第一个展示的页面。我在项目中遇到过,有人把登录页设成了 startDestination,结果用户每次冷启动都要先闪一下登录页——这体验就有点糟糕了。

我的建议:startDestination 尽量选一个「轻量级」的页面,比如首页或者 Splash 页。登录页应该通过条件判断来跳转,而不是直接设为起点。

2. Safe Args:类型安全传参,告别 Bundle 噩梦

以前用 Fragment 传参,都是写 bundle.putString("key", value),然后在目标 Fragment 里 getArguments().getString("key")。你想想看,key 写错了怎么办?类型传错了怎么办?编译时完全不知道,运行时直接崩。

Safe Args 就是来解决这个问题的。它会在编译时生成一个 Directions 类和 Args 类,参数类型、名称都给你检查得死死的。

首先,在 build.gradle 里加上插件:

plugins {
    id 'androidx.navigation.safeargs.kotlin'
}

然后在导航图里定义参数:

<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.DetailFragment">
    <argument
        android:name="itemId"
        app:argType="integer"
        android:defaultValue="0" />
    <argument
        android:name="itemName"
        app:argType="string"
        android:defaultValue="" />
</fragment>

跳转的时候,直接调用生成的 Directions 类:

val action = HomeFragmentDirections.actionHomeToDetail(itemId = 42, itemName = "测试商品")
findNavController().navigate(action)

接收参数也简单:

val args: DetailFragmentArgs by navArgs()
val itemId = args.itemId
val itemName = args.itemName
注意:Safe Args 要求每个参数都有默认值,或者标记为 nullable。我曾经因为忘记给参数设默认值,结果编译报错找了半天——其实错误信息已经写得很清楚了,只是我没仔细看。

3. 深层链接(DeepLink):从外部直接打开指定页面

DeepLink 说白了就是「外部链接直接跳进 App 内部页面」。比如用户收到一条推送,点一下直接跳到商品详情页,而不是先打开首页再跳转。

在导航图里配置 DeepLink 非常简单:

<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.DetailFragment">
    <deepLink app:uri="myapp://detail/{itemId}" />
    <argument
        android:name="itemId"
        app:argType="integer" />
</fragment>

然后在 AndroidManifest.xml 里给 Activity 添加 intent-filter:

<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="detail" />
    </intent-filter>
</activity>

这样,当用户点击 myapp://detail/123 时,系统会自动打开 DetailFragment,并把 itemId 设为 123。

避坑指南:我曾经在 DeepLink 上踩过一个坑——参数类型不匹配。比如 URI 里传的是字符串 "123",但参数声明的是 integer,结果解析失败。解决办法是在 DeepLink 里用 {itemId} 占位符时,确保类型和参数声明一致。

4. BottomNavigation 与 Navigation 联动:底部导航的正确姿势

BottomNavigation 加 Navigation 组件,这俩搭配起来堪称「天作之合」。但很多人实现的方式不对,导致页面切换时 Fragment 被重复创建,或者返回栈混乱。

正确的做法是:每个 Tab 对应一个独立的导航图(NavGraph),然后用 NavHostFragment 来承载。

首先,在布局里放一个 NavHostFragment

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
    app:layout_constraintTop_toTopOf="parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph" />

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottom_navigation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:menu="@menu/bottom_nav_menu" />

然后在 Activity 里设置联动:

val navController = findNavController(R.id.nav_host_fragment)
val bottomNavigation = findViewById<BottomNavigationView>(R.id.bottom_navigation)

bottomNavigation.setupWithNavController(navController)

就这么简单?嗯,但有个坑要注意:setupWithNavController 默认会处理选中状态和页面切换,但如果你在导航图里把多个 Tab 的页面放在同一个导航图里,返回栈会乱套。

我的做法:每个 Tab 单独一个导航图文件,比如 nav_home.xmlnav_discover.xmlnav_profile.xml。然后在主导航图里用 include 把它们组合起来。这样每个 Tab 的返回栈互不干扰。

5. 知识体系总览

下面这张图,是我自己总结的 Navigation 组件核心知识结构。你把它存下来,以后遇到导航相关的问题,先看这张图定位问题在哪一层。

Navigation 组件知识体系 Navigation 组件 导航图设计 Safe Args 传参 深层链接 底部导航联动 startDestination action 定义 多图嵌套 编译时检查 Directions 类 默认值设置 URI 匹配 intent-filter 参数解析 setupWithNavController 独立导航图 返回栈管理

这张图把 Navigation 的四个核心模块拆开了。你从中间往外看,每个模块都有对应的子知识点。我个人建议,先从「导航图设计」入手,把页面关系理清楚,再逐步加上传参、DeepLink 和底部导航联动。

总结一下:Navigation 组件不是银弹,但它确实解决了 Android 导航中 80% 的痛点。导航图让页面关系可视化,Safe Args 让传参不再提心吊胆,DeepLink 打通了内外链接,BottomNavigation 联动让底部导航变得优雅。这四个点掌握好了,你的 App 导航架构基本就稳了。

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