一、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,结果用户每次冷启动都要先闪一下登录页——这体验就有点糟糕了。
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
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。
{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 的页面放在同一个导航图里,返回栈会乱套。
nav_home.xml、nav_discover.xml、nav_profile.xml。然后在主导航图里用 include 把它们组合起来。这样每个 Tab 的返回栈互不干扰。
5. 知识体系总览
下面这张图,是我自己总结的 Navigation 组件核心知识结构。你把它存下来,以后遇到导航相关的问题,先看这张图定位问题在哪一层。
这张图把 Navigation 的四个核心模块拆开了。你从中间往外看,每个模块都有对应的子知识点。我个人建议,先从「导航图设计」入手,把页面关系理清楚,再逐步加上传参、DeepLink 和底部导航联动。
公众号:蓝海资料掘金营,微信 deep3321