Jetpack之Navigation

该文章已生成可运行项目,


Navigation 翻译成中文就是导航的意思,是Jetpack的一个组件,其目的主要就是来管理页面的切换和导航。
官网地址

优势

  • 可视化的页面导航图
  • 通过destinationhe action完成页面间导航
  • 方便添加页面切换动画
  • 页面间类型安全的参数传递
  • 通过NavigationUI类,对菜单、底部导航抽屉菜单导航进行统一管理
  • 支持深层链接DeepLink

缺点

  • 需在nav_graph.xml提前配置所有页面的节点,不易维护
  • 需在nav_graph.xml配置固定的startDestination启动节点,不够灵活
  • Fragment类型的路由导航使用的replace,会使得页面频繁重建,体验不够好
  • Fragment singleTop启动模式,即便目标页在栈顶,依然会销毁后再重建,体验不友好

组成

  1. Navigation Graph (导航图) 是一种新型xml资源,包含应用程序所有页面及导航路径
  2. NavHostFragment 是特殊的Fragment,可以理解成其他Fragment的“容器”。
  3. NavController 是Java/Kotlin对象,完成具体页面的切换工作。

三者关系

当你想要切换Fragment时,使用NavController对象,告诉它你想要去Navigation Graph中的哪个Fragmet,NavController会将你想取的Fragment展示NavHostFragment中

使用

1. 添加依赖

    implementation 'androidx.navigation:navigation-fragment:2.4.1'
    implementation 'androidx.navigation:navigation-ui:2.4.1'

2. 在res目录下创建导航图(xml文件)

依次选中res文件夹–>New–>Android Resource File,新建一个Navigation Graph;
在这里插入图片描述
将File name 设置为“nav_graph”,Resource type(选择)设置为 “Navigation”
在这里插入图片描述
点击Ok就可以了。
在这里插入图片描述
生成的nav_graph.xml文件与普通的布局文件类似,

3 NavHostFragment添加

NavHostFragment是一个特殊的Fragment,我们一般将其添加到Activity、Fragment,此时添加到Activity种,作为其他Fragment的容器

在这里插入图片描述

说明这是一个特殊的Fragment

android:name="androidx.navigation.fragment.NavHostFragment"

设置为true,则该Fragment会自动处理系统返回键。

app:defaultNavHost="true"

用于设置Fragment对应的导航图

app:navGraph="@navigation/nav_graph"

4. 创建destination

在这里插入图片描述
双击“Click to add a destintion”
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
destination 是“目的地”,就是要去的页面,可以是Fragment或Activity,常见的是Fragment。
运行效果
在这里插入图片描述

5 Fragment 页面切换

用同样的方式创建SecondFragment,然后,单击mainFragment,用鼠标选中右侧的圆圈,并拖拽之右边的SecondFragment,松开鼠标。
在这里插入图片描述
会出现一个从mainFragment指向SecondFragment的箭头
在这里插入图片描述

该属性表示指定起始destaination为mainFragment.

app:startDestination="@id/mainFragment"

在这里插入图片描述
看到多了一个action标签

<action
            android:id="@+id/action_mainFragment_to_secondFragment2"
            app:destination="@id/secondFragment" />

app:destination=“@id/secondFragment” 属性表示它的目的地是SecondFragment

fragment_main.xml文件在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6 添加页面切换效果

在res文件下,创建anim文件,
在这里插入图片描述

动画
slide_in_from_left

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromXDelta="-100%"
    android:toXDelta="0" />

slide_in_from_right

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromXDelta="100%"
    android:toXDelta="0" />

slide_out_from_left

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromXDelta="0"
    android:toXDelta="-100%" />

slide_out_from_right

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromXDelta="0"
    android:toXDelta="100%" />

这里就不查看效果了。

7 使用safe args传递参数

Fragment 的切换通常是伴随着参数的传递,为了配合使用Navigation组件在切换Fragment时传递参数,As提供了safe args插件。
Frament间最常见的传递参数和接受参数的方法

  • 传递参数
        Bundle bundle = new Bundle();
        bundle.putString("name", "Alan");
        bundle.putString("age", "30");
       Navigation.findNavController(v).navigate(R.id.action_mainFragment_to_secondFragment2, bundle);
  • 接收参数
        Bundle arguments = getArguments();
        if (arguments != null) {
            String name = arguments.getString("name");
            int age = arguments.getInt("age");
        }

safe args呢?

  1. 添加safe args 插件,在根目录下build.gradle中添加
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
  1. 在 app的build.gradle中添加添加对safe args的依赖
apply plugin 'androidx.navigation.safeargs.kotlin'

3.在导航图中添加标签,可以在Text中编写,也可以通过Design进行添加。

	    <argument
        android:name="user_name"
        android:defaultValue="Alan"
        app:argType="string" />

    <argument
        android:name="age"
        android:defaultValue="18"
        app:argType="integer" />

添加完会在app/generatedJava目录下看到 safe args 插件生成的代码文件了。

需要在Fragment中用插件生成的代码,在Fragment直接传递参数。

传递参数


        Bundle bundle new MainFragmentArgs().Builder()
                .setUerName("Alan")
                .setAge(18)
                .build()
                .toBundle();

        Navigation.findNavController(v)
                .navigate(R.id.action_mainFragment_to_secondFragment2,bundle); 
        

接受参数

        Bundle bundle1 = getArguments();
        if (bundle1 != null){
            String userName = MainFragmentArgs().fromBundle(getArguments().getUerName());
            int ages =  MainFragmentArgs().fromBundle(getArguments().getAge());
        }

safe args 好处在于安全的参数类型。

8 适用场景

  1. 单Activity多Fragment架构
    适用于采用单个Activity嵌套多个Fragment的UI架构模式,通过可视化导航图管理Fragment切换,避免手动处理FragmentManager和FragmentTransaction的复杂性。
    支持页面间类型安全的参数传递和统一的切换动画。
  2. 复杂导航模式
    可处理从简单按钮点击到应用栏、抽屉式导航栏等复杂交互,确保一致的用户体验。
    通过NavigationUI类统一管理菜单、底部导航栏和抽屉菜单。
  3. 遵循导航原则
    默认实现固定的起始目的地、返回堆栈管理,确保向上/返回按钮行为符合用户预期(如返回按钮退出应用,向上按钮返回任务内上一级)。
    支持深层链接(Deep Linking),处理从外部应用启动的导航流程(Deep Link 支持端内、端外跳转,作用与Activity的隐式意图十分相似)。
  4. 开发效率提升
    提供Safe Args Gradle插件,减少参数传递的样板代码。
    集成Android Studio的可视化工具,便于设计导航路径和调试。

9 Controller常用APIs

一、核心导航方法:navigate()
用于从当前目的地导航到新的目的地(如 Fragment/Activity),有多个重载方法,适配不同场景。

  1. navigate(@IdRes int resId)
    参数:resId 是导航图中目的地(fragment/activity)或动作()的 android:id(如 R.id.detail_fragment 或 R.id.action_home_to_detail)。
    作用:跳转到指定 ID 的目的地,无参数传递。
// 跳转到标签页Fragment
navController.navigate(R.id.tags_fragment)
  1. navigate(NavDirections directions)
    参数:directions 是 Safe Args 生成的导航方向对象(如 HomeFragmentDirections.actionHomeToDetail(id=1)),包含目标目的地 ID 和参数。
    作用:携带参数跳转到目标目的地(类型安全,推荐)。
// 生成带参数的导航方向(假设action_home_to_detail需要id参数)
val directions = HomeFragmentDirections.actionHomeToDetail(id = 100)
navController.navigate(directions) // 携带id=100跳转
  1. navigate(Uri deepLink)
    参数:deepLink 是深层链接 URI(如 Uri.parse(“myapp://example.com/detail?id=1”))。
    作用:通过深层链接跳转到匹配的目的地(用于外部唤起应用内页面)。
val uri = Uri.parse("myapp://example.com/detail?id=1")
navController.navigate(uri) // 通过深层链接跳转到详情页
  1. navigate(@IdRes int resId, Bundle args, NavOptions navOptions)
    参数:
    resId:目标目的地 / 动作 ID;
    args:通过 Bundle 传递的参数(替代 Safe Args 的手动传参方式);
    navOptions:导航配置(如动画、返回栈处理)。
    作用:手动传递参数并配置导航选项(如跳转动画、清除返回栈)。
val args = Bundle().apply { putInt("id", 100) }
val options = NavOptions.Builder()
    .setEnterAnim(R.anim.slide_in_right) // 进入动画
    .setExitAnim(R.anim.slide_out_left)  // 退出动画
    .build()
navController.navigate(R.id.detail_fragment, args, options)

二、返回栈管理:popBackStack()
用于从返回栈中弹出页面(即 “返回” 操作),有多个重载方法。

  1. popBackStack()
    参数:无。
    作用:弹出返回栈顶部的页面,回到上一个目的地(类似按系统返回键)。
// 返回到上一页
navController.popBackStack()
  1. popBackStack(@IdRes int destinationId, boolean inclusive)
    参数:
    destinationId:目标目的地 ID(要返回到的页面);
    inclusive:是否包含 destinationId 本身(true 则弹出该页面及以上,false 则保留该页面)。
    作用:返回到指定目的地,并按规则清理返回栈。
// 返回到首页(R.id.home_fragment),并包含首页(即清除首页之上的所有页面)
navController.popBackStack(R.id.home_fragment, true)
  1. popBackStack(@IdRes int destinationId, boolean inclusive, boolean saveState)
    参数:
    saveState:是否保存被弹出页面的状态(如输入框内容),后续重新导航到该页面时恢复。
    作用:更精细地控制返回栈弹出时的状态保存。

三、获取当前状态:getCurrentDestination()
作用:获取当前显示的目的地(NavDestination 对象),可用于判断当前页面。
返回值:NavDestination?(当前目的地,可能为 null if 未初始化)。

val currentDest = navController.currentDestination
if (currentDest?.id == R.id.home_fragment) {
    // 当前在首页
}

四、设置导航图:setGraph()
重载方法:
setGraph(@NavigationRes int graphResId):通过资源 ID 设置导航图;
setGraph(NavGraph graph):通过 NavGraph 对象动态设置导航图。
作用:初始化或动态修改导航图(默认在布局中通过 app:navGraph 设置,代码中可动态替换)。

// 动态设置导航图
navController.setGraph(R.navigation.new_nav_graph)

五、监听导航变化:addOnDestinationChangedListener()
参数:OnDestinationChangedListener 监听器,回调包含:
controller:当前 NavController;
destination:新到达的目的地;
arguments:传递到新目的地的参数。
作用:监听目的地切换事件(如更新标题栏、隐藏 / 显示底部导航)。

navController.addOnDestinationChangedListener { _, destination, _ ->
    when (destination.id) {
        R.id.detail_fragment -> {
            // 进入详情页,隐藏底部导航
            bottomNav.visibility = View.GONE
        }
        else -> {
            // 其他页面,显示底部导航
            bottomNav.visibility = View.VISIBLE
        }
    }
}

六、清除返回栈:clearBackStack()
参数:@IdRes int destinationId(要保留的目的地 ID)。
作用:清除返回栈中指定目的地之上的所有页面(仅保留该目的地及以下)。

// 清除首页之上的所有页面(确保返回时直接退出应用)
navController.clearBackStack(R.id.home_fragment)

七、获取返回栈条目:getBackStackEntry()
参数:@IdRes int destinationId(目标目的地 ID)。
作用:获取指定目的地在返回栈中的条目(NavBackStackEntry),用于传递返回数据(如 Fragment 结果回调)。

// 监听详情页返回的数据
val detailBackStackEntry = navController.getBackStackEntry(R.id.detail_fragment)
detailBackStackEntry.lifecycleScope.launchWhenResumed {
    detailBackStackEntry.savedStateHandle.getLiveData<Int>("result").observe(viewLifecycleOwner) { result ->
        // 处理详情页返回的result
    }
}

  • Navigation 中的 状态保存机制本身不是独立的 “栈”,而是 依托返回栈(Back Stack)的 “关联式存储机制”—— 状态是 “依附于返回栈中的页面条目” 保存的,返回栈的 “栈结构”(先进后出)决定了状态的 “保留 / 销毁规则”,但状态本身不会单独形成一个独立的 “状态栈”。
    简单说:返回栈是 “容器”(栈结构),状态是 “容器里每个元素的附属数据”,不是另一个单独的栈。
    一、核心逻辑:状态与返回栈的 “一对一绑定”
    Navigation 中每个页面(Fragment/Activity)在返回栈中对应一个 NavBackStackEntry(返回栈条目),状态保存的核心是:
    • 每个 NavBackStackEntry 自带一个 savedStateHandle(状态存储容器),页面的状态(如输入框内容、滚动位置)会保存在对应的 savedStateHandle 中;

    • 状态的生命周期与 NavBackStackEntry 完全一致:

      – 页面入栈(navigate()):创建 NavBackStackEntry,状态开始关联保存;
      – 页面出栈(popBackStack()):NavBackStackEntry 被销毁,对应的状态也会一并销毁;
      – 页面被 “休眠”(如底部导航切换,页面仍在返回栈中):NavBackStackEntry 保留,状态也保留。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

10 FragmentContainerView

一个自定义View继承FrameLayout,与ViewGroup不同,它只接受Fragment Views;改进了对fragment z-ordering的处理(Z轴排序优化),优化了两个Fragment退出和进入过度不会互相重叠,使用FragmentContainerView将先开启退出动画然后才是进入动画。

FragmentContainerView 是替代 FrameLayout 作为 Fragment 容器的最佳选择,它通过专门优化解决了传统容器的诸多问题,推荐在所有使用 Fragment 的场景中优先使用。使用时需注意与 AndroidX 库配合,并正确管理 Fragment 事务与容器 id。

11 为什么默认用 replace 而非 add?

对于Fragment 类型的路由节点,在navigate跳转的时候,使用的是FragmentTransaction 的replace
方法切换Fragment的。

Navigation 选择 replace 作为默认切换方式,核心是为了符合 “导航” 的核心诉求:

  1. 避免 Fragment 叠加:若用 add,新 Fragment 会覆盖在旧 Fragment 之上(即使设为不可见,旧 Fragment 仍在内存中),可能导致布局层级混乱、内存泄漏;
  2. 简化状态管理:replace 会销毁旧 Fragment(触发 onDestroyView 或 onDestroy,取决于是否保留实例),仅保留当前目标 Fragment,减少多 Fragment 共存的状态同步问题;
  3. 贴合用户认知:导航场景中,用户通常认为 “跳转新页面后,旧页面暂时‘消失’”,replace 的行为与这种认知一致(如从 “首页” 跳 “分类页”,首页 Fragment 被替换,而非叠加)。
特性Navigation 的 navigate手动 FragmentTransaction.replace
导航栈管理自动将当前 Fragment 入栈,支持返回键回退(popBackStack)需手动管理栈(如 addToBackStack),否则返回键直接退出 Activity
状态恢复配置变更(如旋转屏幕)时,自动恢复导航栈和 Fragment 状态配置变更(如旋转屏幕)时,自动恢复导航栈和 Fragment 状态 需手动保存 / 恢复 Fragment 状态,易丢失
参数传递支持 Safe Args 类型安全传参,自动解析参数需手动通过 Bundle 传参,易出错(如类型不匹配)
转场动画可在导航图中配置 enterAnim/exitAnim,自动生效需手动调用 setCustomAnimations 配置动画
深层链接支持自动处理深层链接跳转,匹配对应 Fragment 节点需手动解析 Intent,判断跳转目标,逻辑繁琐

12 特殊场景:如何让 navigate 用 add 而非 replace?

默认是 replace,但可通过 “保留 Fragment 实例”+“自定义导航行为” 实现类似 add 的效果(如 “底部 Tab 切换,保留每个 Tab 的 Fragment 状态”):

  1. 在导航图中,为目标 Fragment 配置 app:launchSingleTop=“true” 和 app:popUpTo=“@id/xxx”(控制栈行为);
  2. 在代码中,通过 FragmentNavigator.Extras 或自定义 Navigator,修改默认的 replace 为 add(需谨慎,避免叠加问题);
  3. 更推荐的方式:使用 ViewPager2 + FragmentStateAdapter 管理 Tab,而非纯 Navigation 跳转(Navigation 更适合 “线性导航”,Tab 切换更适合 “平行页面”)。

13 架构概述

在这里插入图片描述

总结

Navigation 的 navigate 对 Fragment 节点的跳转,底层确实基于 FragmentTransaction.replace,但通过封装解决了手动 replace 的诸多痛点(如栈管理、状态恢复、参数传递)。日常开发中,无需关注底层 replace 细节,直接通过 navigate 或 Safe Args 跳转即可,Navigation 会自动处理后续逻辑。

代码
什么是Jetpack
LifeCycle之ProcessLifeCycleOwner

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值