Jetpack Navigation 真香预警

Posted 沈页

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Navigation 真香预警相关的知识,希望对你有一定的参考价值。

1. Navigation到底该如何正确的使用

相信大家对 Navigation都有所耳闻,我不细说怎么用了,官方的讲解也很详细。我是想说一下到底该如何更好的使用这个组件。

这个组件其实是需要配合官方的MVVM架构使用的,ViewModel+LiveData结合才能更好的展现出Navigation的优势。

在官方的讲解示例中没有用到ViewModelLiveData,官方只是演示了Navigation怎么用怎么在页面之间传值,和这个组件的一些特性之类的。但真正用好还是要结合ViewModelLiveData

2. Navigation大家都以为的缺陷

起初我用Navigation的时候,最头疼的是当按下返回键回到上个页面的时候整个页面被重建了,这是开发中不想要的结果,很多时候大家都会去寻求一种方式:将官方的replace方式替换为HideShow。起初也是想到这个方式,然后结合在网上得到的资料自己写了一个方式FragmentNavigatorHideShow

3. 然而这不是缺陷

但是很快啊,我发现这个方式(HideShow)存在严重的逻辑问题。

这里可以看到,有一些场景下,我们有某个页面可以打开和自己相同的页面,只不过是展示的数据不同而已。当我用hideshow的方式展示下个页面的时候,会发现打开的还是上个页面。当按下返回键之后,上个相同的页面不见了,新打开的页面和上个页面尽然是同一个对象,这肯定不符合业务逻辑。于是我又开始研究起replace的方式,当然我在使用这个Navigation的时候就采用了MVVM + ViewModel+LiveData,这时候我想起ViewModel是不受Fragment重建影响的。于是我打印了一下在使用replace方式下页面生命周期的变化。

HomeFragment进入MyFragment生命周期变化:

可以看到,在replace之后HomeFragment并没有执行onDestory而是执行了onDestoryView这也使得页面必须要重建。而onDestoryView不会导致 ViewModel的销毁。也就是说 ViewModel还在,ViewModel中的LiveData所保存的数据也是存在的。当我按下返回键,重新回到HomeFragment页面理所当然的执行了onViewCreated,此时代码中页面对ViewModel中的LiveData所观察数据又重新进行了observe观察,因为LiveData之前保存过数据所以这段代码也理所当然的被执行了。页面上也重新填充了数据。

    override fun initLiveData() 
        viewModel.liveData.observe(this) 
            Log.d(TAG, "data change :  $it ")
            textView.text = it
        
    

这个时候,你会发现,页面好像没有重建一样。我这才理解了谷歌的用意。它这步棋下的很巧啊。

也里所当然的我抛弃了FragmentNavigatorHideShow,又拥抱回了谷歌爸爸。

说回上面那个问题,当一个页面中可以打开自己的时候,在FragmentNavigator源码中只要是导航到下一个目的地就会重新创建一个新的Fragment,上一个Fragment会被加入回退栈里,所以才可以在Fragment中打开一个新的自己,来展示不同的信息。而hideshow的方式会每次都去查找之前有没有创建过这个页面,如果有,就Show,如果没有就创建。所以才会导致自己打开自己,永远都是同一个Fragment对象。

4. 那么到底该如何正确使用

到底该如何正确使用Navigation,这也是我这段时间使用的一点点经验。

Fragment中的所有动态数据都由ViewModel中的LiveData保存。我们只监听LiveData的数据变化,这也符合MVVM 的架构麻,当然还有一个Model我没说Repository,这个我就不解释了。

Fragment之间传递的数据都交给Bundle页面重建的时候这些数据也会被保存,再次走一遍从Bundle中取数据的过程是完全不会报错的。所以页面上的数据不会被丢失了,而像RecyclerView,ViewPager之类的控件它们也会保存自己之前的状态,页面重建后,RecyclerView,ViewPager会记录自己滑动的位置的,这个不用担心,还有一点就是有一些控件,比如CoordinatorLayout你可能需要给它和它的子View控件一个Id才能保存滑动状态。

遵循这样的一个规则之后呢,就可以忽略这个页面重建的问题了。

5. Navigation的页面转场动画的一些问题

用过Navigation的都知道,页面转场动画要一个一个的添加,就像这样:

<!--这是官方的Demo示例-->
<fragment
            android:id="@+id/title_screen"
            android:name="com.example.android.navigationsample.TitleScreen"
            android:label="fragment_title_screen"
            tools:layout="@layout/fragment_title_screen">
        <action
                android:id="@+id/action_title_screen_to_register"
                app:destination="@id/register"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"/>
        <action
                android:id="@+id/action_title_screen_to_leaderboard"
                app:destination="@id/leaderboard"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"/>
    </fragment>

每一个标签都要写一遍一样的代码,让我很头疼。于是我还是想到了,重写FragmentNavigator将所有的增加一个判断如果标签中没有设置专场动画,那么我就给这个Fragment添加上专场动画。

      	//我一开始设想的载源码位置处添加的动画操作
		int enterAnim = navOptions != null ? navOptions.getEnterAnim() : 动画id;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : 动画id;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : 动画id;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : 动画id;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) 
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        

然而我太天真了,我们想到的,谷歌爸爸都考虑过了。因为如果像我一样天真的加上这样的判断之后,你会发现,第一个默认Fragment也拥有了动画属性。而且做隐式链接跳转的时候,这个动画会非常影响观感。所以第一个默认Fragment不能有转场动画。当然后来我想到了判断返回栈是否存在为空,通过这个判断是否是第一个页面。但是我都能想到谷歌爸爸肯定也想到了。他们不这么做肯定是有原因的吧。还是等待官方优化,于是我放弃了,老老实实的挨个复制粘贴,

不过后来我在Navigationissues 找到了这个问题,因该在优化的计划中吧。

6. Replace在重建Fragment的时候,过度动画卡顿

在使用 Navigation的时候,按下返回键回到上个页面,页面重建,这个时候会发现过度动画会有那么几百毫秒卡那么一下,一个转场动画也就400毫秒左右,卡那么一下效果是非常明显的。这也归功于Fragment重建的原因了,页面展示的数据量巨大的时候,重建时的绘制工作量也是相当的大,所以肯定会卡那么一下下啦。

后来我发现了一个方法:

    override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? 
        return super.onCreateAnimation(transit, enter, nextAnim)
    

我们可以把数据加载的过程放在动画执行之后再请求。

    override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? 
       if (enter) 
           if (nextAnim > 0) 
               val animation = AnimationUtils.loadAnimation(requireActivity(), nextAnim)
               animation.setAnimationListener(object : Animation.AnimationListener 

                   override fun onAnimationEnd(animation: Animation?) 
                       onEnterAnimEnd()//动画结束后再去请求网络数据、或者初始化LiveData
                   

               )
               return animation
            else 
               onEnterAnimEnd()
           
        else 
           if (nextAnim > 0) 
               return AnimationUtils.loadAnimation(requireActivity(), nextAnim)
           
       
       return super.onCreateAnimation(transit, enter, nextAnim)
   

   /**
    * 子类重写,判断是否需要加载数据,或者初始化LiveData
    */
   fun onEnterAnimEnd()
       Log.d(TAG, "onEnterAnimEnd: ")
   

然后我们再找到onViewCreated方法,因为Base类我们通常会将初始化方法进行抽象所以我们要进行两个事情:

1: 在View进行绘制初始化的时候暂停过场动画

2: 在View与Data初始化结束后再开始动画的执行


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
       super.onViewCreated(view, savedInstanceState)
       //暂停过场动画
       postponeEnterTransition()
       //View与数据初始化
       initViewAndData(view)
       initLiveData()//LiveData的初始化可以放到动画结束之后
        //最后使用这个方法监听视图结构,并开始执行过场动画
       (view.parent as? ViewGroup)?.apply 
           OneShotPreDrawListener.add(this)
               startPostponedEnterTransition()
           
       
   

这样操作之后就可以预防由于RecyclerView大量数据加载时导致的过场动画掉帧问题了,但是也不是完全不掉帧,不过这个解决办法还是有效的,剩下的就是优化自己代码了,防止做太多的耗时操作。

我以前的解决办法是将过场动画进行延时100毫秒执行,但这个方式,我自己也是不满足,于是还是翻阅官方的文档,查找解决办法,掘友也给我提出过相关的问题,这段时间不忙了,所以重新修改一下以前自己犯的问题。


Android高级开发系统进阶笔记、最新面试复习笔记PDF,我的GitHub

文末

您的点赞收藏就是对我最大的鼓励!
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!

以上是关于Jetpack Navigation 真香预警的主要内容,如果未能解决你的问题,请参考以下文章

板刷bzoj计划(真香预警)

JetPack架构---Navigation的使用

jetpack之navigation

jetpack之navigation

jetpack之navigation

jetpack之navigation