• 五月
  • 04
  • load

Jetpack Navigation的一般使用与一些坑 2022-05-04 18:04:51

前排提醒:面对流行或官方框架,请不要为使用而使用,小心掉坑里爬不上去。jetpack特色:不看源码就没法用好这个框架。

先产生第一个问题,这是什么?导航?

对就是导航,用于管理fragment的导航(2.4.0开始,也能用于管理compose)。非常适合用于单activity多fragment架构的场景。

最简单的理解就是让你的fragment当做activity使用,毕竟fragment性能比activity强。

平时我们用的多activity架构有什么问题?确实没有问题,但是,activity是一个重量级组件,里面有一大堆东西恐怕你是用不上的。

而fragment同样可以做到渲染界面的能力,性能比activity更好(那就理解成轻量级activity吧?)

用过fragment的朋友都知道,它比activity难用多了,更考验你对fragment的理解,这玩意总能触发你意想不到的事情。

管理你的fragment,甚至是一学问,明明有了FragmentManager却要在此基础上封装出一个管理的框架。

18年,也许官方意识到,单activity架构逐渐流行起来,因此发布了便于管理的框架,Navigation。

也许这时你会产生一个疑问,那Navigation是可以替代FragmentManager吗?

不能,有些业务你还是必须要使用FragmentManager,这个后面我会举个例子。

Navigation本身并不难,甚至可以摸一会就会使用。但并不意味着能把它用好,这里面的坑非常多,即使阅读了源码也不知道还有多少坑没挖出来。

 

废话说完了,开始说实践吧。

本文仅以kotlin介绍,java使用navigation的方式和kotlin有一定区别,详情请阅读官方文档。

当前版本稳定版为2.4.2,alpha版是2.5.0。本文主要介绍的是2.4.2。

添加依赖:

在顶层gradle添加

 

app gradle顶部处

以上非必选,它们主要作用是让你的参数传递更加安全。

依赖引入

开始,先介绍基本的使用,然后挖坑:

在res目录下创建navigation目录

在navigation目录下右键,New->Navigation Resource File,创建任意名字的xml,这里我就叫nav_main.xml

创建fragment,我就创建三个fragment和一个抽象fragment

布局:

在你的main_activity里,改成这样

其中name和defaultNavHost几乎要写死,不过我个人实际业务中需要改写

导航只会影响到FragmentContainerView,如果你限制了一个高度,在最底下添加一个导航栏,也是可以的,毕竟本质上只是一个fragment

创建导航,打开我们创建的nav_main.xml

点击左上角的New Destination,把我们刚刚创建那三个fragment添加进来

添加导航,我们first要跳second,second要跳third,那就把线拉过去

关于跳转动画:

在默认情况下,页面跳转是没有跳转动画的,不过可以自己设置。

打开nav_main,双击需要跳转的actions,可以得到以下页面

只有none?其实,这需要你创建四个动画,先在res创建anim目录,并创建四个动画

enter_anim:

 

exit_anim:

 

pop_enter:

 

pop_exit:

 

根据名字,对号入座即可。就是需要每个页面都要应用。

pop to:就是修改退栈的位置。

例如,1打开2,2打开3,然后你在2上pop to设置为1,那么打开3并返回时,不会返回到2,而是1。

Inclusive:一旦勾了,返回所指定的页面,也会一起pop掉。

Single Top:我相信大家对这个不陌生,就是你想象的那样。依次打开1->2->3->1,由于1存在了,那么把1提出来,2和3pop掉。

关于跳转

只是为了跳转很简单:

跳转带参数:

如果你没有使用safeargs,那么会麻烦些:

A页面传参:

B页面接收:

参数名需要一一对应,还需要注意数据类型,一不小心,就会闪退。

但你用了safeargs,情况就不一样了

打开nav_main,选择你要接收参数的页面

点击右边的Arguments的+

填入需要接收的参数

A页面发送:

B页面接收:

这样,我们就不用关心接收的时候需要选择什么数据类型了

 

通过代码修改nav_main预设的动画

相信你看过上面的动画设置时会产生一个疑问,“我可以设置默认值吗?一个个设置太麻烦了”

第三个参数,传递的是NavOptions对象,一旦修改,nav_main将失效,可以用于修改动画效果

直接查源码,命名是一一对应的,这里就不再重复介绍了

那么,只要封装一下跳转过程就可以免设置添加跳转动画。

什么?你不想封装?没关系,在挖坑环节下,我会引导如何不封装也能设置默认动画。

 

关于返回:

返回带参数:

Navigation没有这个方法,不过Navigation整个导航都是挂在某一个activity上,因此fragment之间数据是可以利用activity共享数据的,返回传值方法很多,这里我用liveData实现。

在A页面写上:

在B页面退栈时写上:

 

深层链接:

深层链接分为显式深层链接和隐式深层链接。

显式深层链接(PendingIntent方式):

适合做收到消息推送,用户点击时跳转到指定页面。

 

如果是应用内使用,这个跳转过程是非常蛋疼的。

如果你的app只有一个activity,期待跳转到B页面,那么流程就是,先再次打开MainActivity,然后打开A页面(假设A是首页),再然后打开B页面,最后销毁A页面。

而这个activity的launchMode,最好始终为standard。如果用了singleTop或singleTask,有可能就不会跳转了,甚至还可能把最主要的activity给销毁了。

当然,也有解决方案,只要把对应的activity加上一段代码即可

由于显式深层链接本质就是创建PendingIntent对象,基本局限于消息推送和桌面部件了。

隐式深层链接(URI方式):

适用于以浏览器环境,以某种自定义协议唤醒app

例如在浏览器点击超链接sqsx://张三_24,会打开app并跳转某个页

首先,打开你的导航图,点击右侧的deep links

参数:

这些都不是必填的,只要选一个填,匹配上了就能唤醒

一般用的都是URI,因为只有这个可以带参数。

auto verify的作用,说白了就是确认应用是否持有网站地址的拥有权,而且不适用于深层链接。

详细可以参考官方说明“https://developer.android.google.cn/training/app-links/verify-site-associations”

URI可以不声明协议,不声明视为http和https

例如我只填写sqsxblog.com,那么它视为我填写了http://sqsxblog.com 和 https://sqsxblog.com

不过我们现在用的是自定义协议,那么就叫sqsx://就好。

接着,打开AndroidManifest.xml,在activity添加这一段<nav-graph android:value="@navigation/nav_main" />

关于传参:

现在我的B页面,存在name和age的参数,那么我这个URI,必须存在这两个参数,否则会闪退。

那么,我就如此填写sqsx://{name}_{age}

那么,只要以 sqsx://张三_24 点击链接,就会唤醒应用,并自动打开B页面。

打开这个链接,分应用内打开,还有应用外打开。

应用内打开:

那么,它会跳转到能匹配的页面。

应用外打开(浏览器):

只要用户点击这个超链接,就会打开应用,并自动跳转到所匹配的页面

 

挖坑环节:

前面提到,动画没有全局默认值,你需要逐个设置或者封装navigate,这是第一个坑。

当你1打开2,你会发现,切换页面并没有把1放进某个栈里,而是直接销毁了。这是第二个坑

返回时,又重新构建了出来。

某种意义上来说,内存是挺节约的,但从业务上来讲,基本是不能接受这种设定的。

如何解决?

阅读过官方文档,并没有解决方案,那么只好观察底层是怎么处理的。

首先,为什么我们的FragmentContainerView,name必须固定为androidx.navigation.fragment.NavHostFragment?

那我们先看看NavHostFragment是怎么实现的。

点进去发现,它本质上只是一个fragment,那就好办了,直接看生命周期就行。

在onCreate中,引人注目的是它调用了onCreateNavHostController,控制栈进入弹出应该与这个有关

点两下可以看到这段代码

这个createFragmentNavigator是必然调用的,那么看看这个是干嘛的

就创建了一个FragmentNavigator对象,那看看这个对象是干嘛用的。

进入后,你可以发现跳转与返回的方法,在这里都能看见。

那么看看navigate方法你会发现这一段

这意味着你需要传个NavOptions对象,才能修改动画效果。

然后在此的下一行是

直接把fragment替换了,也没有预留设置的方法。。

既然原因知道了,那就对症下药。。

目标是把替换改成切换,那么只好继承这个类然后重写这个方法了。

不过仅改成切换是不行的,因为FragmentManager切换一个fragment,是不会回调fragment任何生命周期,所以我们还要手动调用一下生命周期。

在java目录下创建一个navigation包(名字随意)。

在此目录下,创建一个继承FragmentNavigator的类,类名叫ReusingFragmentNavigator。

创建一个继承NavHostFragment的类,名为ReusingHost

最后,FragmentContainerView的name,由androidx.navigation.fragment.NavHostFragment改为navigation.ReusingHost

至此,既解决了没有默认动画问题,也解决了切换会销毁前置页面问题。

但也会因此产生新的问题,你不能随意去更新Navigation,每次更新必须要对比与新版的差异。因此,我提供的仅仅是一种思路,并不是最佳解决方案。

该问题18年就被提起,21年才被官方确定,至今,最新的2.5.0,alpha04,依然没有解决这个问题。我们能做的,只能期待官方去处理这个问题。

 

关于页面管理:

Navigation是以链表形式存储栈的,栈越靠底越消不掉(这么说其实不算严谨,深层链接的案例就实现把栈底消掉)

例如打开1,2,打开3时把1和2销毁,3作为栈底存在,这是不行的,常见案例是欢迎页->登录->首页

解决方案也有,根据你的栈底情况,在navigation目录下创建多个导航即可

例如欢迎页一个,登录注册忘记密码一套,首页等主体一套,当栈底需要移除的时候,就替换掉FragmentContainerView的navGraph即可

切换方法:findNavController().setGraph(navigation资源)

 

关于navigation-ui,为什么我不用?

如果你的应用不需要顶部应用栏(AppBarLayout)、抽屉式导航栏(DrawerLayout)和底部导航栏(BottomNavigationView),那么引入navigation-ui没有任何意义。你也别想用于自定义组件上,navigation-ui只支持这三个组件。

如果你不想要切换动画,可以打消这个念头,它写死了。

切换效果,以BottomNavigationView为例,它这个切换,是以压栈形式完成的,这意味着,你由1切换为2,然后2切换为1,那么这个2,会被pop掉。

虽说这些问题完全可以改代码解决,但它的作用本身就很小,我却需要付出更大的工程量处理这个问题。因此,我个人是不喜欢用的。

navigation-ui的问题同时也说明了,navigation不适合用于布局类似主界面的bottom navigation

这种情况建议用回FragmentManager进行管理,开头提到过,不要为使用而使用,navtgation并不能替代FragmentManager

 

最后我想说的是,我不觉得这是一个优秀的框架,管理fragments即使是一门学问,但也不至于为此不惜代价而实现。总之,看需求使用吧

代码已传入https://gitee.com/sqsxtest/NavigationDemo

原创文章,转载请注明出处

正在加载评论...

0 / 240

警告

确定