国际惯例,官网镇楼
developer.android.com/guide/navig…
很多人在学习JetPack的时候喜欢到处找资料和各种学习的博客,但其实,官网上的资料已经很丰富了,而且写的很好,大部分时间,只需要先将官网上的资料吃透,基本上已经秒杀市面上80%的博客和文章了。
这篇文章并不会花大篇幅讲解Navigation的各种使用,因为官网文档已经无比详细了,本篇文章更重要的是讲解设计原理和核心概念的分析。
Navigation是JetPack中非常重要的一员,他对现代化的Android JetPack架构,提供了基础,是构建整体架构的核心组件。同时,Navigation也是一个优秀的Fragment管理工具(当然,不仅仅是管理Fragment,Activity也是可以的),可以很好的处理之前使用Fragment那些不是很好的方面,通过Navigation,开发者可以将重点放在业务开发上,避免处理太多了Fragment管理代码和调用代码,从而加速业务开发效率。
- 提供了Fragment管理容器
- 支持Deeplink、URL Link定位到Fragment
- Fragment、Activity间更加安全的参数传递
- 更加方便的处理过渡动画
使用Navigation主要需要创建以下几个部分的代码:
- Navigation Graph:用于对Fragment进行配置的配置文件,需要在res/navigation/下创建的xml文件
- FragmentContainerView/NavHostFragment:一系列Fragment的容器,用于承载Fragment
- NavController:用于处理Fragment路由跳转
下面通过一个简单的例子,来演示下,如何使用Navigation。
引入依赖
implementation "androidx.fragment:fragment-ktx:1.2.0"
implementation "androidx.navigation:navigation-fragment-ktx:2.3.0"
implementation "androidx.navigation:navigation-ui-ktx:2.3.0"
复制代码
创建测试Fragment和Activity
class LoginFragment : Fragment(R.layout.fragment_login) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
复制代码
类似这样的测试Fragment,不浪费笔墨了。
创建Navigation Graph
在res文件夹下创建navigation文件夹,并定义一个xxxx.xml文件,选择类型为navigation。
这时候,将测试的Fragment导入Design视图,就可以看见这些Fragment的界面了,通过每个视图左右拉出来的箭头,就可以生产一个路由Action,如图所示。
通过可视化界面,可以很清楚的看见Fragment间的路由路径,同时要注意的是,单个Fragment可以生成不止一个Action,例如一个Fragment可以跳转多个其他Fragment。
通过Design生成的代码如下所示。
对于navigation标签来说,最重要的是它的startDestination属性,即类似MainActivity的概念,代表了路由的起点。多个destination连接起来就组成了一个栈导航图,destination之间连接就是action。
每个fragment标签,代表了一层路由,当然,这里不仅仅可以是fragment,也可以是Activity、Dialog。
在每个fragment标签里面的action标签,就代表路由的具体行为,destination就是该路由的终点。
创建Activity并引入NavHostFragment
在Activity的xml布局中,通过FragmentContainerView来创建这些Fragment的容器,代码如下所示。
FragmentContainerView是一个特殊的Fragment,只能添加Fragment,
- app:navGraph:这里需要指定前面在res文件夹下创建的navigation文件
- app:defaultNavHost="true":代表可以拦截系统的返回键,用来托管路由
- android:name="androidx.navigation.fragment.NavHostFragment":代表这个容器就是用来管理Fragment的容器
FragmentContainerView内部会通过反射的方式,初始化名为name所指定的class——NavHostFragment,它就是所有需要管理的Fragment的Container。
在NavHostFragment中,有两个重要的参数,即mGraphId和mDefaultNavHost,保存着我们从xml中解析出来的数据。同时,在onCreate的时候,创建了NavController,与mGraphId进行绑定。
使用路由
在Fragment中,可以通过NavController来进行路由,代码如下所示。
class LoginFragment : Fragment(R.layout.fragment_login) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
login.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_loginFragment_to_registerFragment)
}
}
}
复制代码
同时,也可以通过Bundle来进行参数的传递,这跟之前使用Fragment基本类似,代码如下。
Navigation.findNavController(it).navigate(R.id.action_registerFragment_to_mainListFragment, bundleOf("name" to "xuyisheng"))
复制代码
所以这里可以很方便的进行路由选择,针对不同的判断条件,选择不同的路由action。
为什么能获取
这里有个地方很有意思,那就是为什么通过view可以获取NavController。
Navigation.findNavController(View)
复制代码
从源码中可以发现。
实际上,他是从Tag中取出的,而这个Tag,则是在NavHostFragment的onViewCreated中创建的。
这样的API设计,可以让用户传入View后进行遍历,通过查找指定Tag来获取NavController,简化了调用方式。
路由跳转
通过NavController进行路由跳转,有多种方式,比如通过路由action指定,也可以指定跳转的destination。
action
这就是前面提到的路由方式,也是最常用的路由方式,代码如下所示。
Navigation.findNavController(it).navigate(R.id.action_loginFragment_to_registerFragment)
复制代码
不过要注意的是,使用action进行路由跳转,要保证当前页面的实例是存在的,否则会抛出异常。
destination
直接使用destination的id,同样可以跳转到指定的destination,代码如下所示。
Navigation.findNavController(it).navigate(R.id.mainListFragment)
复制代码
这种方式,同样是创建一个新的页面实例。
返回控制
路由的返回控制,有两种方式,navigateUp和popBackStack。下面通过一个例子来演示下,如何对路由进行返回控制,下面有三个Fragment,A-B-C。
navigateUp
navigateUp与物理返回键的功能类似,即返回当前页面堆栈的栈顶页面,代码如下所示。
Navigation.findNavController(it).navigateUp()
复制代码
当我们从A路由到B,B路由到C后,通过上面的代码,使用navigateUp返回,则路由返回路径为C到B,B到A,如果在A继续调用navigateUp,则不会响应,因为当前栈中只有唯一一个页面,而且是startDestination,所以不会再响应返回操作。
popBackStack
navigateUp只能响应向上一级的路由控制,而不能跨级进行路由返回,popBackStack则是对其的补充,可以指定路由返回的action,代码如下所示。
Navigation.findNavController(it).popBackStack(R.id.loginFragment, true)
复制代码
当我们从A路由到B,B路由到C后,通过popBackStack返回,指定要返回到的Fragment的id,即可直接返回到指定位置,第二个参数inclusive,代表返回操作是否包含指定的Fragment id。
这里要注意的是,当你指定返回到A,同时inclusive为true的时候,A也是不会被移除的,因为A是栈顶。
实际上,navigateUp内部就是通过popBackStack实现的。
借助popBackStack的返回值,可以在跳转失败时,创建新的Fragment。
val flag = Navigation.findNavController(getView()).popBackStack(R.id.someFragment, false)
if (!flag){
Navigation.findNavController(getView()).navigate(R.id.someFragment)
}
复制代码
defaultNavHost
app:defaultNavHost="true"这个属性是我们最早在FragmentContainerView中设置的,通过这个属性,可以让当前的NavHostFragment拦截系统的返回键,也就是说,只要当前Fragment堆栈中有元素,就拦截系统返回键,用于Fragment堆栈的出栈,直到堆栈中只剩下一个元素,则将系统返回值的功能交还给Activity。
popupTo
当我们通过navigation去进行路由的时候,每次都会创建一个新的实例,所以,当navigation出现下面的循环图时,如下所示。
这样的循环图,会导致页面路由变成这样A—B—C—A—B—C,这就导致页面栈中存在了大量重复的页面。
所以在这种场景下,就需要在A—B—C之后,在C—A的路由中,配置popUpTo="@id/A",同时设置popUpToInclusive=true,将旧的A界面也移除,这样,C—A路由之后,页面栈中就只剩下A了(如果是false,则会存在两个A的实例),代码如下所示。
<fragment
android:id="@+id/mainListFragment"
android:name="com.example.navigation.MainListFragment"
android:label="MainList">
<action
android:id="@+id/action_mainListFragment_to_loginFragment"
app:destination="@id/loginFragment"
app:popUpTo="@id/loginFragment"
app:popUpToInclusive="true" />
</fragment>
复制代码
再考虑下面这样一个场景,A—B,B路由到C的时候,设置popUpTo="@id/A",如果popUpToInclusive=false,则跳转到C之后的路由栈为A—C,如果设置为true,则只剩下A在路由栈中,代码如下所示。
<fragment
android:id="@+id/registerFragment"
android:name="com.example.navigation.RegisterFragment"
android:label="Register">
<action
android:id="@+id/action_registerFragment_to_mainListFragment"
app:destination="@id/mainListFragment"
app:popUpTo="@id/loginFragment"
app:popUpToInclusive="true" />
</fragment>
复制代码
这个场景可以使用于登录注册之后跳转主页的场景,当跳转主页后,就应该把登录和注册的界面pop出栈。
所以,从上面的实例就可以分析出,在action中配置popUpTo属性,指的是在当前路由中,一直将页面出栈,直到指定的页面为止,而popUpToInclusive,则是代表包含关系,是否包含指定的页面。
个人感觉这个API命名为popUntil可能更合适一点。
在代码中,也存在类似的调用方法。
NavOptions.Builder() .setPopUpTo(R.id.fragmentOne, true) .build() 复制代码
Navigation动态加载
除了在xml中设置navGraph,有很多场景下,我们会根据业务场景动态设置一些navGraph,或者某些navGraph是需要动态获取一些参数之后才去初始化的,这时候,就可以使用Navigation的动态加载方案。
首先,需要在Fragment容器中,去掉navGraph的引用,然后在Activity中,动态指定要引用的navGraph,代码如下所示。
// 动态加载
val navHostFragment = supportFragmentManager.findFragmentById(R.id.navFragmentHost) as NavHostFragment??:return
val navigation = navHostFragment.navController.navInflater.inflate(R.navigation.nav_graph_base)
navigation.startDestination = R.id.loginFragment
navHostFragment.navController.graph = navigation
复制代码
实际上和动态Inflate布局再添加布局到容器的场景非常类似,Navigation动态加载也是将navGraph从xml中创建好之后设置给navigation,接收参数的话,与正常的参数传递是一样的。
添加路由动画
路由切换动画是action的属性,当我们使用action进行路由时,可以指定目标Page,和原Page的动画切换效果,它包含下面几个属性。
- enterAnim:目标Page进入动画
- exitAnim:目标Page进入时,原Page退出动画
- popEnterAnim:目标Page退出动画
- popExitAnim:目标Page退出时,原Page退出动画
有点绕,但是这个和原来Activity间使用的overridePendingTransition是一样的。这里的动画,可以通过在Design界面中,直接选中action来设置,也可以直接在代码中指定。设置好后,代码如下所示。
动画文件比较简单,就是常见的补间动画。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="-100%"
android:toXDelta="0%"
android:fromYDelta="0%"
android:toYDelta="0%"
android:duration="700" />
</set>
复制代码
在代码中,这些动画是通过NavOptions来承载的,并赋值给navigate()的参数。
总结
Navigation的引入,是Google在JetPack上下的第一步棋,通过Navigation,Google指明了在JetPack下Android开发的大方向:
- 单Activity架构:Google这次重写了Fragment,希望能回到设计它的初衷,从目前来看,整个方向是对的
- 申明式编程:将原始的命令式编程,向神明式编程转变,将逻辑申明出来,这很挑战老程序员的思维转变
- 为其它组件铺路:Navigation的架构,适合与其它组件组合使用,例如,虽然每次都会创建Fragment的实例,但是通过LiveData来共享和恢复数据
总的来说,Navigation组件为新的现代化Android开发铺平了道路,但是要在现有的工程基础上进行改造,则成本是比较大的,大家应该先掌握Navigation的设计思想,这样可以更好的掌握其它JetPack组件。