1 、基本概念
1.1、背景
采用单个Activity嵌套多个Fragment的UI架构模式,已经被大多数的Android工程师所接受,需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。
在Android中,页面的切换和管理包括应用程序Appbar的管理、Fragment的动画切换以及Fragment之间的参数传递等内容。并且,纯代码的方式使用起来不是特别友好,并且Appbar在管理和使用的过程中显得很混乱。因此,Jetpack提供了一个名为Navigation的组件,旨在方便开发者管理Fragment页面和Appbar。
相比之前Fragment的管理需要借助FragmentManager
和FragmentTransaction
,使用Navigation组件有如下一些优点:
- 可视化的页面导航图,方便我们理清页面之间的关系
- 通过destination和action完成页面间的导航
- 方便添加页面切换动画
- 页面间类型安全的参数传递
- 通过Navigation UI类,对菜单/底部导航/抽屉蓝菜单导航进行统一的管理
- 支持深层链接DeepLink
1.2、含义
- Navigation 是 Android Jetpack 组件包 中的重要一员,借助于 Single Activity 和 多个Fragment 碎片,优化 Android Activity 启动的开销和简化 Activity 之间的数据通信问题。
- 内置支持普通 Fragment、Activity 和 DialogFragment 组件的跳转,也就是所有 Dialog 或PopupWindow 都建议使用 DialogFragment 实现,这样可以涵盖所有常用的跳转场景,统一返回栈的管理。
- 另外,基于 Fragment 实现可以做到状态存储和恢复。
假设你是一名传统的基于 Activity 开发者,现在想迁移到 Navigation 导航架构,你一定会下面几个疑问:
- 全都用 Fragment?那原本 Activity 跳转的启动类型 (singleTask、singleTop) 如何提供支持?
- Fragment 之间的如何传递数据?
- 原本 onActivityForResult 现在应该如何实现一个 “onFragmentResult” ?
2、组成
2.1、Navigation graph
一个包含所有导航相关信息的 XML 资源
- xml 档,包含所有被管理的 Fragment,起始目标,换页目标,返回目标。
2.2、NavHostFragment
一种特殊的Fragment,用于承载导航内容的容器
<fragment
android:id="@+id/navHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNaHost="true"
app:navGraph="@navigation/nav_graph_main" />
2.3、NavController
管理应用导航的对象,实现Fragment之间的跳转等操作
- 用来管理 NavHost 中的导航动作,通常是写在点击事件内完成 Fragment 的切换。
textView.setOnClickListener {
findNavController().navigate(R.id.action_Fragment1_to_Fragment2)
}
3、基本使用
3.1、引入依赖
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'
3.2、创建导航视图
首先确保AndroidStudio为3.3以上
- 1.右键
res
,点击New -> Android Resource Directory
- 2.在出现的面板第二行
Resource type
下拉列表中选择Navigation
,然后点击OK
- 3.res目录下会多出一个
navigation
的资源目录,右键该目录,点击New -> Navigation Resource File
,输入需要新建的资源文件名,这里命名nav_graph
,点击ok,一个nav_graph.xml
就创建好了。
3.3、配置graph:添加fragment
新建好的nav_graph.xml切换到design模式下,点击2处的加号,选择Create new destination,即可快速创建新的Fragment,这里分别新建了FragmentA
、FragmentB
、FragmentC
三个fragment
建好后,可通过手动配置页面之间的跳转关系,点击某个页面,右边会出现一个小圆点,
拖曳小圆点
指向跳转的页面,这里设置跳转的关系为FragmentA -> FragmentB -> FragmentC
。
切换到Code栏,可以看到生成了如下代码
<?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/fragmentA">
<fragment
android:id="@+id/fragmentA"
android:name="com.example.testnavigation.FragmentA"
android:label="fragment_a"
tools:layout="@layout/fragment_a" >
<action
android:id="@+id/action_fragmentA_to_fragmentB2"
app:destination="@id/fragmentB" />
</fragment>
<fragment
android:id="@+id/fragmentB"
android:name="com.example.testnavigation.FragmentB"
android:label="fragment_b"
tools:layout="@layout/fragment_b" >
<action
android:id="@+id/action_fragmentB_to_fragmentC2"
app:destination="@id/fragmentC" />
</fragment>
<fragment
android:id="@+id/fragmentC"
android:name="com.example.testnavigation.FragmentC"
android:label="fragment_c"
tools:layout="@layout/fragment_c" />
</navigation>
navigation
是根标签,通过startDestination配置默认启动的第一个页面,这里配置的是FragmentAfragment
标签代表一个fragment,其实这里不仅可以配置fragment,也可以配置activity,甚至还可以自定义action
标签定义了页面跳转的行为,相当于上图中的每条线,destination定义跳转的目标页,还可以定义跳转时的动画等等- 当调用到 action_FragmentA_to_FragmentB2 这个 action,会从 FragmentA -> FragmentB
3.4、添加NavHostFragment
在MainActivity的布局文件中配置NavHostFragment
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:name
指定NavHostFragmentapp:navGraph
指定导航视图,即建好的nav_graph.xmlapp:defaultNavHost=true
意思是可以拦截系统的返回键,可以理解为默认给fragment实现了返回键的功能,这样在fragment的跳转过程中,当我们按返回键时,就可以使得fragment跟activity一样可以回到上一个页面了
现在我们运行程序,就可以正常跑起来了,并且看到了FragmentA展示的页面,这是因为MainActivity的布局文件中配置了NavHostFragment,并且给NavHostFragment指定了导航视图,而导航视图中通过startDestination指定了默认展示FragmentA。
3.5、通过NavController 管理fragment之间的跳转
上面说到三个fragment之间的跳转关系是FragmentA -> FragmentB -> FragmentC,并且已经可以展示了FragmentA,那怎么跳转到FragmentB呢,这就需要用到NavController 了
打开FragmentA类,给布局中的TextView定义一个点击事件
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv.setOnClickListener {
val navController = Navigation.findNavController(it)
navController.navigate(R.id.action_fragmentA_to_fragmentB2)
}
}
如果发现不能自动导入布局文件,大概率是要给app.build添加插件‘kotlin-android-extensions’
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
}
可以看到,通过navController管理fragment的跳转非常简单,首先得到navController对象,然后调用它的navigate方法,传入前面nav_graph中定义的action的id即可。
按同样的方法给FragmentB中的TextView也设置一个点击事件,使得点击时跳转到FragmentC
运行程序,FragmentA -> FragmentB -> FragmentC
,此时按返回键,也是一个一个页面返回,如果把前面的app:defaultNavHost
设置为false
,按返回键后会发现直接返回到桌面。
3.5.1、NavController 的获取及其能力
上面的例子中,我们通过 Fragment 的扩展方法可以拿到此 Fragment 从属的 NavController,另外还有一些重载的方法:
// 根据 viewId 向上查找
NavController findNavController(Activity activity, int viewId)
// 根据 view 向上查找
NavController findNavController(View view)
本质上 findNavController 就是在当前 view 树中,查找距离指定 view 最近的父 NavHostFragment 对应的 NavController,目前仅做了解即可。
NavController 的能力
对于应用层来说,整个 Navigation 框架,我们只跟 NavController 打交道,它提供了常用的跳转、返回和获取返回栈等能力。
4、跳转时传递参数
4.1、通过带 bundle 参数的 navigate 方法传递参数
通过指定 bundle 参数可以为目的地传递参数,比如:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv.setOnClickListener {
val navController = Navigation.findNavController(it)
val bundle = Bundle()
bundle.putString("key", "test")
navController.navigate(R.id.action_fragmentA_to_fragmentB2, bundle)
}
}
在目的地 Fragment 可以直接通过 getArguments() 方法获取 这个bundle。
super.onCreate(savedInstanceState)
val value = arguments?.getString("key")
...
}
4.2、通过 safeArgs 插件
afe args与传统传参方式相比,好处在于安全的参数类型,并且通过谷歌官方的支持,能很方便的进行参数传值。
1、在项目的根build.gradle
下添加插件
buildscript {
ext.kotlin_version = "1.3.72"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.4"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.0"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
2、然后在app的build.gradle
中引用 'androidx.navigation.safeargs.kotlin'
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'androidx.navigation.safeargs.kotlin'
}
3、添加完插件后,回到nav_graph,切到design模式,给目标页面添加需要接收的参数
这里需要在FragmentA跳转到FragmentB时传参数,所以给FragmentB设置参数,点击FragmentB,点击右侧面板的Arguments右侧的+,输入参数的key值,指定参数类型和默认值,即可快速添加参数
4、添加完后,
rebuild
一下工程,safeArgs会自动生成一些代码,在/build/generated/source/navigation-args
目录下可以看到
safeArgs会根据nav_graph中的fragment标签生成对应的类,
- action标签会以
“类名+Directions”
命名, - argument标签会以
“类名+Args”
命名。
使用safeArgs后,传递参数是这样的
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv.setOnClickListener {
val navController = Navigation.findNavController(it)
//通过safeArgs传递参数
val navDestination = FragmentADirections.actionFragmentAToFragmentB2("test")
navController.navigate(navDestination)
// 普通方式传递参数
// val bundle = Bundle()
// bundle.putString("key", "test")
// navController.navigate(R.id.action_fragmentA_to_fragmentB2, bundle)
}
}
接收参数是这样的
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {
val value = FragmentBArgs.fromBundle(it).key
.......
}
.......
}
5、动画
5.1、action 参数设置动画
enterAnim: 跳转时的目标页面动画
exitAnim: 跳转时的原页面动画
popEnterAnim: 回退时的目标页面动画
popExitAnim:回退时的原页面动画
新增 anim
slide_in_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>
slide_out_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="-100%"
android:toYDelta="0%" />
</set>
slide_in_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="-100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>
slide_out_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="100%"
android:toYDelta="0%" />
</set>
添加到action: 可以根据不同需求使用 alpha、scale、rotate、translate 這几种效果
<action
android:id="@+id/action_page1_to_action_page2"
app:destination="@id/page2Fragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
5.2、共享元素
如果兩個頁面有類似的元素,可以用這種方式讓視覺有連續被帶過去的感覺。
在兩個頁面共用的元件加上 transitionName 這個屬性,屬性的值要一樣。
fragment_one.xml
<ImageView
android:id="@+id/catImageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@mipmap/cat"
android:transitionName="catImage" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="cat"
android:transitionName="catText" />
fragment_two.xml
<ImageView
android:id="@+id/catImageView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:src="@mipmap/cat"
android:transitionName="catImage" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="cat"
android:transitionName="catText" />
PageOneFragment.kt
把 xml 元件的 transitionName 赋值给 NavController
val extras = FragmentNavigatorExtras(
catImageView to "catImage",
textView to "catText")
catImageView.setOnClickListener {
findNavController().navigate(
R.id.action_page1_to_action_page2,
null,
null,
extras)
}
PageTwoFragment.java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSharedElementEnterTransition( TransitionInflater.from(requireContext())
.inflateTransition(R.transition.shared_image));
}
shared_image.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<changeImageTransform/>
</transitionSet>
6、常见问题
1、Fragment 跳转的启动类型 (singleTask、singleTop) 如何提供支持?
栈管理:点击destination,右侧面板中还可以看到popUpTo
、popUpToInclusive
、launchSingleTop
1、launchSingleTop
:如果栈中已经包含了指定要跳转的界面,那么只会保留一个,不指定则栈中会出现两个界面相同的Fragment数据,可以理解为类似activity的singleTop,即栈顶复用模式,但又有点不一样,比如FragmentA@1 -> FragmentA@2,FragmentA@1
会被销毁,但如果是FragmentA@01>FragmentB@02>FragmentA@03
,FragmentA@1
不会被销毁。
2、popUpTo
(tag):表示跳转到某个tag,并将tag之上的元素出栈。
3、popUpToInclusive
:为true表示会弹出tag,false则不会
例子:FragmentA -> FragmentB -> FragmentC -> FragmentA
设置FragmentC -> FragmentA
的action为popUpTo=FragmentA ,popUpToInclusive=false
,那么栈内元素变化为
最后会发现需要按两次返回键才会回退到桌面
设置popUpToInclusive=true
时,栈内元素变化为
此时只需要按一次返回键就回退到桌面了,从中可以体会到popUpTo和popUpToInclusive的含义了吧。
2、Fragment 之间的如何通信?
Fragment 中的通信还可以分为两种场景,假设目前 返回栈中有两个Fragment 分别为 A 和 B。
- 若 A 与 B 在同级子图中,可以在两端通过创建 导航图级别的 ViewModel 完成交互。
例如当前返回栈为
NavGraphA -> NavDestinationB -> NavDestinationC -> NavDestinationD
1、若想实现 C 与 D 的通信,需要使用 可以使用 节点B 创建 ViewModel。
val vm by navGraphViewModels<TitleVm>(R.id.nav_destination_b)
R.id.home 为二者的最近的公共父 Graph,在父 Graph 销毁前,二者通信都是有效的。
2、若 A 与 B 不在同级子图中,可以使用距离二者最近的公共父 Graph 完成通信。
例如当前返回栈为
NavGraphA -> NavDestinationB -> NavGraphC -> NavDestinationD
若想实现 B 与 D 的通信,需要使用 A节点创建 ViewModel。
val vm by navGraphViewModels<TitleVm>(R.id.home)
3、navigation fragment的重绘
3.1、Fragment生命周期
Navigation出现之前官方给出的Fragment生命周期如下图:(注意onDestroyView之处
)
而LIfecycle,Navigation等组件出现之后,官方给出的Fragment生命周期图为下图:(PS:Fragment Lifecycle && View Lifecycle)
Navigation框架下的Fragment生命周期分为
Fragment Lifecycle
和 View Lifecycle
,View Lifecycle被单独拎出来了,原因就在于Navigation
框架下的非栈顶的Fragment
均会被销毁View, 也即是 A跳转到B页面: A会执行onDestroyView销毁其 View
(凡是和View相关的,如:Databinding、RecyclerView都会被销毁) , 但是Fragment本身会存在
( Fragment本身的成员变量等 是不会被销毁的 )
Navigation框架之下的正确状态流转应该是类似这的:
A 通过action打开B,A从
onResume
转到onDestroyView
,B从onAttach
执行到onResume
, 当B通过系统返回键返回到A时候,A从上图的onCreateView流转到onResume
, 此过程中A的View经历销毁和重建,View(binding实例)的对象实例是不一样的,但是Fragment A这个实例始终相同。
这样的场景下,假设A存在一个网络新闻列表RecyclerView, RecyclerView随着View被销毁、重建。如何保存其中的数据,避免每次返回到A的时候重新刷新数据(造成:上次浏览数据、位置丢失、额外的网络资源消耗), 因此RecyclerView中Adapter的数据项非常关键!
常见的保存方式有:
- 1、通过
Fragment
的成员变量 - 2、ViewModel。在ViewModel的ViewModelScope通过协程请求网络数据,保存在ViewModel(ViewModel生命周期贯穿Fragment),可通过LiveData、普通变量保存数据,在
onViewCreated
之后恢复数据
参考
1、安卓navigation系列——入门篇
2、安卓navigation系列——进阶篇
3、Navigation 组件使用入门
4、Android官方架构组件Navigation:大巧不工的Fragment管理框架
5、Navigation-02-Fragment生命周期
6、Fragment 重建现象