概述

Navigation是采用一个Activity和多个Fragment形式设计的Ui架构模式,但是众所周知,Fragment的管理一直是个麻烦事,需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。所以Google提供了一套Navigation用来管理Fragment相互间的跳转等逻辑。我们先看下Navigation的优势:

  • 处理 Fragment 事务。

  • 默认情况下,正确处理往返操作。

  • 为动画和转换提供标准化资源。

  • 实现和处理深层链接。

  • 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。

  • Safe Args - 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。

  • ViewModel支持 - 您可以将ViewModel的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。

正式介绍前,我们需要了解Navigation是由哪几部分组成的,现在我们看一下:

  • NavHostFragment:一种特殊的Fragment,用于承载导航内容的容器。

  • Navigation Graph:一个包含所有导航和页面关系相关的 XML资源。

  • NavController:管理应用导航的对象,实现Fragment之间的跳转等操作。

首先我们在build.gradle 文件中新增navigation的依赖:

1
2
3
// Kotlin
implementation("androidx.navigation:navigation-fragment-ktx:$lifecycle_version")
implementation("androidx.navigation:navigation-ui-ktx:$lifecycle_version")

然后我们需要新建一个Navigation,点击Project->Res->New Android Resource File,Resource Type选择Navigation,如下图:

打开新建的nav_graph.xml文件,在Design界面可以看到目前还没有内容,可以依次点击[New Destination]图标,然后点击[Create new destination],即可快速创建新的Fragment,这里作次示例,因为本身之前我已经新建好多个Fragment了,就不单独示范了。

我们可以点击视图的小圆点,建议页面的绑定关系。我这里已经建立好多个页面的绑定关系,同时你可以切换至code模块,本人代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?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/jetpack_nav"
app:startDestination="@id/navigationFragment">

<fragment
android:id="@+id/navigationFragment"
android:name="com.silence.apmprojetct.NavigationFragment"
android:label="fragment_navigation"
tools:layout="@layout/fragment_navigation">
<action
android:id="@+id/action_navigationFragment_to_blankFragment"
app:destination="@id/blankFragment" />
<action
android:id="@+id/action_navigationFragment_to_threeFragment"
app:destination="@+id/threeFragment" />
</fragment>
<fragment
android:id="@+id/blankFragment"
android:name="com.silence.apmprojetct.SecondFragment"
android:label="fragment_blank"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_blankFragment_to_threeFragment"
app:destination="@id/threeFragment" />
</fragment>
<fragment
android:id="@+id/threeFragment"
android:name="com.silence.apmprojetct.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three">
<action
android:id="@+id/action_threeFragment_to_navigationFragment"
app:destination="@+id/navigationFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
</navigation>

我们看下几个标签的含义:

  • navigation:根标签,通过startDestination配置指定默认的启动页面。
  • fragment: fragment标签代表一个fragment。
  • action:action标签定义了页面跳转的行为,destination标签定义跳转的目标页,里面还有启动模式的相关设置等等。

而讲到这里,我们只概括前面三个模块其中之一Navigation Graph。我们知道Fragment需要一个Activity才能正常运行,但是我们如何在我们的Activity指定我们上面startDestination呢。这就需要用到了NavHostFragment。我们看下我们Activity的布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".NavigationActivity">
<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:navGraph="@navigation/jetpack_nav" />

</FrameLayout>

我们可以看到布局里定义了一个Fragment,并且它使用的就是NavHostFragment,可以看到navGraph属性绑定了我们刚刚写的Navigation文件。而defaultNavHost就是做到了自动实现了页面间的返回操作。如果没有这个属性,点击返回的时候就是直接关闭当前Activity了。当然,其内部Fragment的点击事件我们可以自己控制。

然后我们可以运行程序,可以看到默认展示的是NavigationFragment页面,这是因为NavigationActivity的布局文件中配置了NavHostFragment,并且给NavHostFragment指定了默认展示的页面为NavigationFragment。

NavController主要用来管理fragment之间的跳转,每个NavHost均有自己的相应NavController。然后可以使用它的navigate或者navigateUp方法来进行页面之间的路由操作。比如我这边的NavigationFragment需要跳转SecondFragment。我们可以这么写:

1
2
3
4
5
6
7
8
9
10
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_navigation, container, false)
view.findViewById<TextView>(R.id.next).setOnClickListener {
findNavController().navigate(R.id.action_navigationFragment_to_blankFragment)
}
return view
}

也就是通过navigate指向前面我们在action定义的id。即可完成跳转。

参数传递

Bundle数据传递

我们可以创建一个Bundle对象,然后使用navigate()将它传递给目的地,代码如下:

1
2
3
val bundle = Bundle()
bundle.putString(TITLE, getString(R.string.three_fragment_navigation_bundle_label))
findNavController().navigate(R.id.action_blankFragment_to_threeFragment, bundle)

使用 Safe Args 传递安全的数据

这是官方为了navigation数据传递安全提供的一个数据传递方式,首先我们需要在根目录的build.gradle文件中添加如下依赖:

1
2
3
4
dependencies {
def nav_version = "2.3.3"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}

然后我们在模块的build.gradle文件中新增如下:

1
2
3
4
//kotlin
apply plugin: "androidx.navigation.safeargs.kotlin"
//java
apply plugin: 'androidx.navigation.safeargs'

然后我们ReBuild项目,可以看到在build目录中生成了如下文件:

然后我们修改我们的代码,比如我需要在ThreeFragment页面新增一个参数,那么我们需要在navigation文件中新增如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<fragment
android:id="@+id/threeFragment"
android:name="com.silence.apmprojetct.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three">
<action
android:id="@+id/action_threeFragment_to_navigationFragment"
app:destination="@+id/navigationFragment" />
<argument
android:name="key"
app:argType="string"
android:defaultValue="safeArgs" />
</fragment>

然后我们在需要跳转的时候修改下代码,比如我这里SecondFragment会跳转至ThreeFragment。那么我们只需要这么写:

1
2
3
4
5
6
7
8
        view.findViewById<TextView>(R.id.next).setOnClickListener {
// val bundle = Bundle()
// bundle.putString(TITLE, getString(R.string.three_fragment_navigation_bundle_label))
// findNavController().navigate(R.id.action_blankFragment_to_threeFragment, bundle)
var secondFragment =
SecondFragmentDirections.actionBlankFragmentToThreeFragment("safe传递的数据")
findNavController().navigate(secondFragment)
}

我们在看下如何在ThreeFragment里接收数据:

1
2
3
view.findViewById<TextView>(R.id.title).text = arguments?.let {
ThreeFragmentArgs.fromBundle(it).key
}

其他场景

堆栈处理

前面我们说到了正常的页面启动,以及如何挟带参数跳转。现在有个场景,比如我A-》B-》C之后,然后直接回到A。并且清栈,也就是在A页面返回就直接关闭当前的Activity了。我们看一下,目前我们的代码执行后效果会如何:

可以看出来我们每次打开新的fragment就相当于新开了一个堆栈。我们可记得我们配置页面的时候有一个action属性。我们把action的代码改成如下:

1
2
3
4
5
6
7
8
9
10
11
<fragment
android:id="@+id/threeFragment"
android:name="com.silence.apmprojetct.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three">
<action
android:id="@+id/action_threeFragment_to_navigationFragment"
app:destination="@+id/navigationFragment"
app:popUpTo="@+id/navigationFragment"
app:popUpToInclusive="true" />
</fragment>

我们运行一下,在看一下效果:

哎,可以了。

其实action还有一个属性launchSingleTop的设置,这个看具体场景使用即可。

指定startDestination

我们想一下,如何指定startDestination的Fragment呢。上面介绍过,activity要绑定NavHostFragment。而NavHostFragment要指定startDestination。在我们在navigaiton指定了fragment后,可以修改么。或者说,我可以动态改变他的startDestination嘛。其实是可以的。我们知道startDestination是由navGraph指定的。那么我们只需要修改activity的代码,就可以做到了。具体示例如下:

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
val startDestinationID = R.id.blankFragment
val navController = Navigation.findNavController(this, R.id.fragment)
val navInflater = navController.navInflater
val navGraph = navInflater.inflate(R.navigation.jetpack_nav)
navGraph.startDestination = startDestinationID
navController.graph = navGraph
}

那我们执行一下,看下效果:

我这边指定了SecondFragment成功了。那具体指定哪一个,由你自己的业务场景而定。

总结

本文我们主要介绍了Jetpack中的导航组件Navigation的组件,从普通的跳转,已经挟带参数跳转,甚至一些特殊场景的跳转逻辑。关于一些其他场景,我这里不过多介绍,因为目前正常的跳转场景基本已经了解了。如果你想知道更多的使用场景,请查看官方文档