10

JetPack指路明灯—Navigation

 3 years ago
source link: https://blog.csdn.net/eclipsexys/article/details/110604775
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

国际惯例,官网镇楼

https://developer.android.com/guide/navigation

很多人在学习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,如图所示。

IjEZBvI.png!mobile

通过可视化界面,可以很清楚的看见Fragment间的路由路径,同时要注意的是,单个Fragment可以生成不止一个Action,例如一个Fragment可以跳转多个其他Fragment。

通过Design生成的代码如下所示。

UFVNbaq.png!mobile

对于navigation标签来说,最重要的是它的startDestination属性,即类似MainActivity的概念,代表了路由的起点。多个destination连接起来就组成了一个栈导航图,destination之间连接就是action。

每个fragment标签,代表了一层路由,当然,这里不仅仅可以是fragment,也可以是Activity、Dialog。

在每个fragment标签里面的action标签,就代表路由的具体行为,destination就是该路由的终点。

创建Activity并引入NavHostFragment

在Activity的xml布局中,通过FragmentContainerView来创建这些Fragment的容器,代码如下所示。

ZFnIvi3.png!mobile

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)

从源码中可以发现。

bYnA3uB.png!mobile

实际上,他是从Tag中取出的,而这个Tag,则是在NavHostFragment的onViewCreated中创建的。

ZziUbeM.png!mobile

这样的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出现下面的循环图时,如下所示。

77Z7Vj.png!mobile

这样的循环图,会导致页面路由变成这样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来设置,也可以直接在代码中指定。设置好后,代码如下所示。

Nfaem2V.png!mobile

动画文件比较简单,就是常见的补间动画。

<?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组件。

更多内容,欢迎大家访问我的网站

https://xuyisheng.top


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK