再讲Navigation-基于Navigation实现单Activity+多Fragment保存/恢复实例的几种流派分析

2,403 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

Navigation保存实例的几种方案分析

关于单Activity+多Fragment的系列文章我已经出了几期了,关于单Activity+多Fragment的框架实现,目前比较推荐的做法是基于 Navigation 来实现此功能。

之前的文章我们讲过 单Activity+多Fragment的框架选型 。讨论并实现了Navigation的封装。解决了Navigation中通信问题。大家如果有兴趣可以回顾一下。

那么 Navigation 的使用是不是只有这一种方案呢? 保存/恢复 Fragment 的实例的方法有没有别的方案呢?如果有总共有哪些流派?大家使用都是基于哪一种流派实现的呢?有什么优缺点呢?

一、原生的使用

我们在讲 Navigation 的实例保存/恢复的示例之前我们先用原生的 Navigation 实现一次,并且打印生命周期。

如果你能看到此文,那我相信各位高工们应该不是初步接触 Navigation,或多或少应该都会基础的使用的,这里关于基本的使用我就快速过了,如果实在是想了解 Navigation 的基础使用与源码解析,推荐去看 Navigation使用与源码剖析

Activity的xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:viewBindingIgnore="true">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        app:navGraph="@navigation/nav2"
        app:defaultNavHost="true" />

</FrameLayout>

Navigation配置文件-导航图

<?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"
    app:startDestination="@id/page1Fragment">

    <fragment
        android:id="@+id/page1Fragment"
        android:name="com.guadou.kt_demo.demo.demo11_fragment_navigation.nav2.Nav2Fragment1"
        android:label="fragment_page1"
        tools:layout="@layout/nav2_fragment1">
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment" />
    </fragment>

    <fragment
        android:id="@+id/page2Fragment"
        android:name="com.guadou.kt_demo.demo.demo11_fragment_navigation.nav2.Nav2Fragment2"
        android:label="fragment_page2"
        tools:layout="@layout/nav2_fragment2">
        <action
            android:id="@+id/action_page3"
            app:destination="@id/page3Fragment" />
    </fragment>

    <fragment
        android:id="@+id/page3Fragment"
        android:name="com.guadou.kt_demo.demo.demo11_fragment_navigation.nav2.Nav2Fragment3"
        android:label="fragment_page3"
        tools:layout="@layout/nav2_fragment3" >

    </fragment>

</navigation>

然后就可以使用了,以 Fragment2 为例:

    override fun initViews(view: View) {

        view.findViewById<TextView>(R.id.btn_gotopage1).click {
            Navigation.findNavController(getView() ?: view).navigateUp()
        }


        view.findViewById<TextView>(R.id.btn_gotoPage3).click {
            Navigation.findNavController(getView() ?: view).navigate(R.id.action_page3, null)
        }

    }

跳转我们使用action 返回我们使用 navigateUp 。我们操作从 Fragment1 到 Fragement2 到 Fragment3 然后返回到 Fragment2 再返回到 Fragment1 。

打印的生命周期Log如下:

可以看到默认是无法保存实例的,EditText的值都没了,内部的View都重新创建了,那么现在就开始我们的操作了,如何保存实例。

二、使用ViewModel恢复数据

看生命周期,虽然是走的OnCreateView onDetstoryView ,说明此 Fragment 也只是销毁了View,恢复的时候再创建View。但是 Fragment 还是那个 Fragment 。它内部持有的ViewModel没变,它内部持有的成员变量值也没有变。

我们是不是可以直接通过成员变量恢复值呢?

    <EditText
        android:id="@+id/et_one"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/d_40dp" />
class Nav2Fragment1 : BaseVDBFragment() {

   var oneTextValue: String = ""   //赋值成员变量管理

   override fun init() {

        if (!TextUtils.isEmpty(oneTextValue)) {
            mBinding.etOne.setText(oneTextValue)
        }

        //监听ET的值
        mBinding.etOne.addTextChangedListenerDsl {
            onTextChanged { charSequence, i, i2, i3 ->
                oneTextValue = charSequence.toString()
            }
        }

    }
}

效果:

那为什么谷歌推荐我们使用ViewModel来保存这些数据?我们选择一下屏幕就知道,这些数据会被清掉了,如果内存不足,一样会重走生命周期导致成员变量会回收,所以推荐我们使用ViewModel来保存数据。

那我们现在进阶一下,如果我们使用 ViewModel 搭配 DataBinding 那... 自动赋值,自动保存,自动恢复 ,岂不是完美?

试试?

@HiltViewModel
class Nav2ViewModel  @Inject constructor():BaseViewModel() {

    var mEtOneText = MutableLiveData<String>()

    var mEtTwoText = MutableLiveData<String>()

}

我们保存前两个页面的EditText的值。在Fragment 中我们直接绑定 DataBinding 配置

class Nav2Fragment1 : BaseVDBFragment<EmptyViewModel, Nav2Fragment1Binding>() {

    private val activityViewModel by activityViewModels<Nav2ViewModel>()

    override fun getDataBindingConfig(): DataBindingConfig {

        return DataBindingConfig(R.layout.nav2_fragment1, BR.viewModel, activityViewModel)
    }
}

在页面中就是很普通的 DataBinding xml 文件。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:binding="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="RtlHardcoded">

    <data>

        <variable
            name="viewModel"
            type="com.guadou.kt_demo.demo.demo11_fragment_navigation.nav2.Nav2ViewModel" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <EditText
            android:text="@={viewModel.mEtOneText}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/d_40dp" />

        <Button
            android:id="@+id/btn_gotoPage2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/d_40dp"
            android:text="去第二个页面" />

    </LinearLayout>

</layout>

没有做任何别的操作,看看效果

这???ViewModel + DataBinding 就自动就适配 Navigation 的实例保存问题了?

是的,这也是谷歌推荐 MVVM 框架的原因,这也是 DataBinding 的魅力所在,真是YYDS!

当然我们这里Demo是最简单的例子,如果有比较复杂的数据需要恢复,我们可以自行在ViewModel中恢复,或者自定义 DataBindingAdapter 的方式来实现。这么讲下去就脱离话题了,有机会讲DataBinding的时候再细讲。

三、使用View的缓存来恢复

MVVM 框架是好用,但是我不会,或者我觉得太麻烦不想用,有没有更简单的方法?

有!试试这种方法。

之前我们有讲到既然是走的 OnCreateView onDetstoryView ,说明此 Fragment 也只是销毁了View, 恢复的时候再创建View。那我们把此 Fragment 的视图View保存不就行了吗?

我们保存一份根视图,当重建的时候走 OnCreateView ,我们再还给他不就行了?

其他的代码不需要修改,我们直接改 Fragment 的基类。

代码如下:

abstract class AbsFragment : Fragment(), ConnectivityReceiver.ConnectivityReceiverListener {

    private var isRootViewInit = false
    private var rootView: View? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        if (rootView == null) {
            rootView = transformRootView(setContentView(container))
        }

        return rootView

    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        if (!isRootViewInit) {//初始化过视图则不再进行view和data初始化
            super.onViewCreated(view, savedInstanceState)
            initViews(view)
            isRootViewInit = true
        }

        initViews(view)
    }


    override fun onDestroy() {
        super.onDestroy()

        isRootViewInit = false
        rootView = null
    }

那我们 Fragment 的代码如第一步的代码,最原始的Fragment,也不用ViewModel,也不用DataBinding。

效果如下

一样的可以达到效果,相比第一步,第二步的优势是,无需再次inflate,解析Xml了,节省了解析布局,填充布局的耗时,但是把View缓存了,更耗费内存了,属于空间换时间的一种。

缺点就是 RootView 在成员变量中保存的,我们无法保障页面恢复的时候能必定恢复到 RootView 。那么这种情况下,我们可以使用 ViewModel 的方式来缓存,或者使用一个全局的或Activity级别的静态容器来缓存,在创建Fragment和销毁Fragment的时候管理静态容器,一样的可以达到我们的效果。

这种方案也是可以用于实战,很好用的一种方案,简单暴力。

四、魔改Navigation

之前我们有讲到既然是走的 OnCreateView onDetstoryView ,说明此 Fragment 也只是销毁了View, 恢复的时候再创建View。我们追根查询为什么会这样?

大家应该都知道 Navigation 的原理就是 FragmentTransaction 的 replace 方法。这就引出了这一种方案,改造 Navigation 的方案,修改为 show / hide 的方案。

之前的文章我已经分享过,如何改造 Navigation,如何封装 Navigation 的路由表,封装跳转,返回等处理。

其实改造 Navigation 大家都改造程度不同,有的方案只是修改 show / hide ,跳转还是使用原生的。

有的改造就把路由表的生成改了,觉得当项目大了,维护一个庞大的路由表太麻烦了,有些人通过定义跳转的方法手动的创建路由表,有些人通过Fragment上加注解的方式,通过注解生成器来生成对应的路由表,生成的方式不同,但是殊途同归,都是为了去掉 Navigation 的 xml 配置文件。

再进一步的改造,就是把跳转和返回等事件封装,再进一步就添加路由可以外部直接跳转,等等等等。

我的方案也只是到跳转和返回的封装,通过自定义跳转方法,手动的添加导航图。无需配置复杂的路由表。

关于改造的效果,如果大家有兴趣可以看看我之前的文章。这里不过多赘述。

总结

目前主流的就是这三种流派来实现的,其中如有一些变化,基本不脱离这三个方向。

到底用哪一种流派比较好?

如果你就想用原生的 Navigation 我推荐你使用 DataBinding + ViewModel 的方式,这一种方案哪都好,就是会销毁View 和 创建View,如果你的 Fragment 布局很复杂,那么可能恢复的时候会耗时会卡顿。

如果你不会或不想使用 DataBinding ,那也可以使用第二种方案,使用一个容器缓存当前 Fragment 的跟视图,在 onCreateView 的时候恢复。但是缺点是相对占用空间,内存不足或旋转屏幕页面重建的时候会丢失,我推荐使用Activity 级别的静态容器来管理这些跟视图。

如果觉得自己的项目太大,不想维护庞大的路由表,也可以使用魔改 Navigation 的方法,通过 FragmentManager 来自动的帮我们管理Fragment的实例。

能不能一起用,或结合起来使用?

当然不是选择一种方案就放弃另一种了,大家可以三种方案自由选择与组合都是可以的,比如我不改 show / hide。我只是不想写路由表,我通过注解的方式自动生成路由表或者自定义启动方法在内部手动创建路由图,然后我使用第二种方案,使用静态容器来自行管理 Fragment 的 rootView 。或者我通过 DataBinding + ViewModel 来管理实例,都是可以的。

你是哪一挂的?

啊,你问我选择哪一种流派?我是魔改派的。魔改派最方便最简单了 😅 😅 !

哈哈,开个玩笑,各方案各有利弊,大家按实例项目与需求,自行选择即可。

好了,还是那句话,手机红外功能,我可能一辈子都用不到,但是我要有。关于一些功能的实现方案,我可以不用,但是我要会!

本期内容如讲的不到位或错漏的地方,希望各位高工可以指出交流。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。