Navigation-02-Fragment生命周期

Navigation-02-Fragment生命周期

[TOC]

0、 References

Android Navigation 遇坑记 - 真实项目经历

官方文献Navigation

官方文献Fragment

案例仓库BeerMusic gitee.com/junwuming/B…

起初Jetpack Navigation把我逼疯了,可是后来真香

PS: 最好先看一下第一个链接

1、 Fragment lifecycle

Navigation 相关的坑,都有个中心。一般情况下,Fragment 就是一个 View,View 的生命周期就是 Fragment 的生命周期,但是在 Navigation 的架构下,Fragment 的生命周期和 View 的生命周期是不一样的。当 navigate 到新的 UI,被覆盖的 UI,View 被销毁,但是保留了 fragment 实例(未被 destroy),当这个 fragment 被 resume 的时候,View 会被重新创建。这是“罪恶”之源。

版权声明:本文为CSDN博主「 zijietiaodong技术团队」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:blog.csdn.net/bytedancete…

Navigation框架下的Fragment压栈时View会被销毁,再进入栈顶时会再次构建View。这就面临一个Activity不会存在的问题:如何恢复View的数据? 例如:常见的RecyclerView也会在入栈的时候被销毁,当用户返回时候,需要重新构建RecyclerView、绑定Adapter, 因此Adapter或者说是列表数据不能丢失,需要在ViewModel或者Fragment类成员变量中保存, 否则就只能再次请求网络加载数据(体验非常糟糕)。FragmentView分家的问题,用下面的2张生命周期图可以阐释出来:

  • Navigation出现之前官方给出的Fragment生命周期如下图:(注意onDestroyView之处)
  • LIfecycleNavigation等组件出现之后,官方给出的Fragment生命周期图为下图:(PS:Fragment Lifecycle && View Lifecycle)

Navigation框架下的Fragment生命周期分为 Fragment Lifecycle 和 View LifecycleView Lifecycle被单独拎出来了,原因就在于Navigation框架下的非栈顶的Fragment均会被销毁View, 也即是 A跳转到B页面: A会执行onDestroyView销毁其 View凡是和View相关的,如:Databinding、RecyclerView都会被销毁) , 但是Fragment本身会存在( Fragment本身的成员变量等 是不会被销毁的 ) 。为啥这样设计, 请参考Navigation的这个 Issue:Navigation, Saving fragment state , 很多人重写Navigation,使其能够保存View。这个例子请参考 起初Jetpack Navigation把我逼疯了,可是后来真香 ,大致的实现就是将官方的replace方式替换为HideShow。 解决了一部分问题,但是踩过坑的我感觉遗祸无穷: 生命周期、LiveData等导致的潜在、 防不胜防的bug。

Navigation框架之下的正确状态流转应该是类似这的:

image-20211123151604229

A 通过action打开B,A从 onResume转到onDestroyView,B从onAttach执行到onResume, 当B通过系统返回键返回到A时候,A从上图的onCreateView流转到onResume , 此过程中A的View经历销毁和重建,View(binding实例)的对象实例是不一样的,但是Fragment A这个实例始终相同。

这样的场景下,假设A存在一个网络新闻列表RecyclerView, RecyclerView随着View被销毁、重建。 如何保存其中的数据,避免每次返回到A的时候重新刷新数据(造成:上次浏览数据、位置丢失、额外的网络资源消耗), 因此RecyclerViewAdapter的数据项非常关键! 常见的保存方式有: 1、通过Fragment的成员变量 2、ViewModel 。方法2非常合适,在ViewModelViewModelScope通过协程请求网络数据,保存在ViewModelViewModel生命周期贯穿Fragment),可通过LiveData、普通变量保存数据,在 onViewCreated之后恢复数据

2、 项目中的坑

下面内容核心全部来自Android Navigation 遇坑记 - 真实项目经历

1、 Databinding 需要 onDestroyView 设置为 Null。

如下所示的Fragment基类代码片段 中我们看到View的实例 ViewBinding binding是作为类成员变量。JVM中成员变量生命周期是贯穿对象的,因此当Fragment状态流转到 onDestroyView 时并不会主动释放这个binding实例。 当然我们也不要想着保留它以复用,因为在onCreateView再次去inflate了一个新的binding,故此 **千万记得在 onDestroyView销毁View实例 ** 释放掉这部分暂时不会使用的内存: onDestroyView: _binding = null

abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
    companion object {
        private const val LIFECYCLE_TAG = "FragmentLifecycle"
    }

    private var _binding: T? = null
    open val binding get() = _binding!!
	
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        Log.d(LIFECYCLE_TAG, "onCreateView: ${this::class.java.name}@${this.hashCode()}")
        _binding = DataBindingUtil.inflate(inflater, initLayout(), container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        Log.d(LIFECYCLE_TAG, "onDestroyView: ${this::class.java.name}@${this.hashCode()}")
        // Avoid binding leak!!!
        _binding = null
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(LIFECYCLE_TAG, "onDestroy: ${this::class.java.name}@${this.hashCode()}")
    }

    override fun onDetach() {
        super.onDetach()
        Log.d(LIFECYCLE_TAG, "onDetach: ${this::class.java.name}@${this.hashCode()}")
    }

复制代码

2、 当 Databinding 遇到错的 lifecycle

onCreateView 方法中我们使用的是 binding.lifecycleOwner = viewLifecycleOwner 而不是 binding.lifecycleOwner = this ,后者是错误的方式 , 为什么说错?首先我们看一下一个熟悉的MVVM片段:

override fun initObserve() {
        viewModel.time.observe(viewLifecycleOwner){
            // binding UI
        }
    }
复制代码

Fragment在 onViewCreated 添加Observe监听ViewModel的LiveData, 显而易见这个数据是用来刷新UI界面的,因此这的观察者生命周期使用的viewLifecycleOwner而非Fragment本身(View被销毁了,刷新界面毫无意义) ,同理Databinding的实例本身会在binding销毁时候跟着消失,因此根本不需要使用Fragment的生命周期。

更为常见的是如下的方式结合,避免在Fragment写过多视图刷新代码

class TestFragment : BaseNavigationFragment<FragmentTestBinding>() {
    private val viewModel by viewModels<TestViewModel>()
    override fun initLayout(): Int {
        return R.layout.fragment_test
    }

    @SuppressLint("SetTextI18n")
    override fun initView() {
        binding.viewModel = viewModel
    }
}


@RequiresApi(Build.VERSION_CODES.N)
class TestViewModel : ViewModel() {
    private val _time = MutableLiveData("Time-Zone")
    val time: LiveData<String>
        get() = _time

    init {
        viewModelScope.launch {
            val df =
                SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault(Locale.Category.FORMAT))

            while (true) {
                delay(2000)
                _time.value = df.format(Date())
            }
        }
    }
}

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="viewModel"
            type="cn.zhaojunchen.beermusic.ui.mine.TestViewModel" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".ui.mine.BFragment">

        <TextView
            android:text="TimeStrip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:text="@{viewModel.time}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </androidx.appcompat.widget.LinearLayoutCompat>

</layout>

复制代码

但是本质上这2者都是需要Lifecycle的,前者是在代码中指定的View LifecycleOwner,后者所示用的则是 binding.lifecycleOwner = viewLifecycleOwner , 参考前者的写法,后者不难理解了(可以去看一下XMLDatabindingImpl文件),前者若设置 binding.lifecycleOwner = this , 那么在View销毁时候,LiveData是不会反注册的

LiveData.removeObservers(Observer) 其实是在接受生命周期回调的 onStateChanged(LifecycleOwner, Lifecycle.Event) 方法中,会判断如果是如果当前 owner 处于 DESTROYED 状态就执行。

image-20211125103555502

引用文章中的解释:

这段代码运行起来没有问题,看起来都是按照预期的在执行。甚至官方代码也是这么写的。连 LeakCanary 也检测不出来内存泄漏的问题,LeakCanary 只能检测出来一些 Activity,Fragment 和 View 等实例的内存泄漏,对于普通的类的实例是没有办法分析的。

问题就出现在 databinding 遇到了一个错的 lifecycle,在没有用 Navigation 框架的时候,View 的生命周期和 Fragment 的生命周期一致的,但是在 Navigation 框架下,两者的生命周期是不一致的。我们来看下 ViewDataBinding 设置 lifecycleOwner 的具体代码。

下面的代码中,往这个 lifecycleOwner 里面加入了一个 OnStartListener 实例,因为这个 lifecycleOwner 是 fragment 的,会在 fragment 销毁的时候反注册,但是并不会在 View 被销毁的时候被反注册。而 OnStartListener 有对这个 ViewDataBinding 有引用,会导致 View 被销毁的时候(跳到另外一个页面),这个引用会阻止系统回收这个 View。

这个分析逻辑是对的,但是结果是不对的,系统还是会对这个 View 进行回收,因为 OnStartListener 的实例持有的是对这个 View 的弱引用,这个 View 还是会被回收。这就是 LeakCanary 没有报错的原因。但是这个 OnStartListener 的实例,就没这么幸运了,正是这个实例无法回收导致了内存泄漏。


@MainThread
public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
    if (mLifecycleOwner == lifecycleOwner) {
        return;
    }
    if (mLifecycleOwner != null) {
        mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);
    }
    mLifecycleOwner = lifecycleOwner;
    if (lifecycleOwner != null) {
        if (mOnStartListener == null) {
            mOnStartListener = new OnStartListener(this);
            // 这个实例持有了ViewDataBinging的实例,虽然是弱引用。
        }
        lifecycleOwner.getLifecycle().addObserver(mOnStartListener);
        // 问题出现在这里,如果这个lifecycle是fragment的,View被销毁了,里面不会进行反注册。
    }
    for (WeakListener<?> weakListener : mLocalFieldObservers) {
        if (weakListener != null) {
            weakListener.setLifecycleOwner(lifecycleOwner);
        }
    }
}
复制代码

后续的坑大家可以仔细看看, 引入Navigation后基本都会遇到, 特别是ViewPager2 ,这里不细说了。

3、 Glide 自我管理的生命周期值得信赖吗?

4、 Android 组件的生命周期自我管理值得信任吗?

5、 当 ViewPager2 遇到 Navigation

6、 ViewPager2 设置 Adapter 导致的 Fragment 重建问题

7、 在 Navigation 的框架下,手动进行 Fragment 管理需要注意什么?

8、 Navigation 的主持下,Fragment 和 View 分家了,家产怎么分?

3、 View恢复现场 实例代码

前面提到的View销毁重建, 恢复现场就是我们必须考虑的事情。幸运的是,很多View都会保存状态,我们只需要恢复其中的数据。下面演示一个ViewModel、LiveData、RecyclerView状态保存的例子, 源代码请转到BeerMusic HomeFragment

1、 ArticleAdapter 构建列表适配器

使用ListAdapterDiffUtil.ItemCallback构建Adapter, ListAdapter可以使用DiffUtil避免普通Adapter的全量刷新, 更新数据是也不需要考虑是全量刷新、插入式刷新、删除式刷新,在保证高性能的同时又尽可能避免复杂的逻辑控制。

class ArticleAdapter(
    private val longClickCallback: ((ArticleBean?) -> Unit)?
) :
    ListAdapter<ArticleBean, ArticleAdapter.ViewHolder>(ArticleBeanDiffCallback()) {

    private lateinit var context: Context

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.create(parent, longClickCallback)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
        holder.binding.executePendingBindings()
    }


    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        context = recyclerView.context
    }

    class ViewHolder(
        val binding: ItemArticleBinding,
        private val callback: ((ArticleBean?) -> Unit)?
    ) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(articleBean: ArticleBean?) {
            if (articleBean == null) {
                showNoDataBean()
                return
            }
            binding.bean = articleBean
        }

        private fun showNoDataBean() {
            // init the view for null data
        }

        companion object {
            fun create(parent: ViewGroup, callback: ((ArticleBean?) -> Unit)?): ViewHolder {
                val binding: ItemArticleBinding = DataBindingUtil.inflate(
                    LayoutInflater.from(parent.context),
                    R.layout.item_article,
                    parent,
                    false
                )

                binding.root.setOnClickListener {
                    binding.bean?.let { bean ->
                        WebActivity.open(binding.root.context, bean)
                    }
                }

                binding.root.setOnLongClickListener {
                    callback?.invoke(binding.bean)
                    return@setOnLongClickListener true
                }

                return ViewHolder(binding, callback)
            }
        }
    }

    private class ArticleBeanDiffCallback : DiffUtil.ItemCallback<ArticleBean>() {

        override fun areItemsTheSame(oldItem: ArticleBean, newItem: ArticleBean): Boolean {
            return (oldItem.title == newItem.title) && (oldItem.author == oldItem.author)
        }

        override fun areContentsTheSame(oldItem: ArticleBean, newItem: ArticleBean): Boolean {
            return oldItem == newItem
        }
    }

}
复制代码

更新数据时候直接提交即可 adapter.submitList(data.toList())

2、 构建ViewModel保存数据

ViewModel保存列表数据, ListViewModel是通用的RecyclerView数据存储ViewModel,其中 private val _data = MutableLiveData<MutableList<T?>>(mutableListOf()) 使用LiveData存储可观察列表数据,列表上拉时触发分页请求,当网络数据到达时,将数据添加到_data,并触发其刷新。

class HomeViewModel : ListViewModel<ArticleBean>() {

    init {
        fetch(true)
    }

    override val microTask: MicroTask<ArticleBean>
        get() = { Repo.networkService.getHomePageArticle(page = page.pageNum) }

}

abstract class ListViewModel<T> : ViewModel() {
    companion object {
        private const val TIMES_PROGRESS = 600L
    }

    private val _isSwipeRefresh = SingleLiveEvent<Any>()
    val isSwipeRefresh get() = _isSwipeRefresh

    val page = Pages()
    private var _status = MutableLiveData(Status.NONE)
    val status: LiveData<Status>
        get() = _status

    private val _showProgress = MutableLiveData<Boolean>()
    val showProgress: LiveData<Boolean>
        get() = _showProgress

    val showProgressDelay: LiveData<Boolean> = _showProgress.switchMap {
        liveData {
            delay(TIMES_PROGRESS * 10)
            _showProgress.value
        }
    }

    private val _showNoData = MutableLiveData(false)
    val showNoData: LiveData<Boolean>
        get() = _showNoData

    private val _data = MutableLiveData<MutableList<T?>>(mutableListOf())
    val data: LiveData<MutableList<T?>>
        get() = _data

    abstract val microTask: MicroTask<T>?
    
    xxx 通用RecyclerView ViewModel框架
}


复制代码

3、 MVVM下的Fragment

HomeFragment 主页面,在onCreatedView 添加对ViewModel的列表数据监听,而RecyclerView添加加载更多的滑动监听,触发ViewModel不断加载数据,LiveData数据变化时通知主页面刷新,相辅相成。这里使用到了 by autoClearedView销毁时自动销毁Adapter(数据存储在ViewModel,没必要再保留Adapter), autoCleared工具参考这部分代码 , 这里使用ConcatAdapter组合AdapterRecyclerView1.2+),可以轻松组合Adapter, 拿来写加载更多 转圈提示在合适不过,不细说了。

lass HomeFragment : BaseNavigationFragment<FragmentHomeBinding>() {

    private val viewModel: HomeViewModel by viewModels()

    private var headerAdapter
            by autoCleared<ArticleHeaderAdapter>()
    private var adapter
            by autoCleared<ArticleAdapter>()
    private var footerAdapter
            by autoCleared<ArticleFooterAdapter>()

    override fun initLayout(): Int {
        return R.layout.fragment_home
    }

    override fun initView() {
        super.initView()
        headerAdapter = ArticleHeaderAdapter(ArticleHeaderBean("I am the king of the world"))
        adapter = ArticleAdapter { bean ->
            /**
             *  When creating a DialogFragment from within a Fragment, you must use the Fragment's child
             *  FragmentManager to ensure that the state is properly restored after configuration changes.
             *  Please reference the FragmentManager: https://developer.android.google.cn/images/guide/fragments/manager-mappings.png?hl=zh-cn
             * */
            bean?.let {
                openShareSheetFragment(it)
            }
        }
        footerAdapter = ArticleFooterAdapter()

        val concatAdapter = ConcatAdapter(headerAdapter, adapter, footerAdapter)
        binding.recyclerView.adapter = concatAdapter
        /*binding.recyclerView.addItemDecoration(MarginDecoration(6, 6, 8, 8, 6))*/
        binding.recyclerView.addOnScrollListener(RecyclerViewUtil.getRecyclerViewScroller {
            XLog.d("scroller the bottom")
            viewModel.getArticleBeanList()
        })
    }

    override fun initObserve() {
        super.initObserve()
        viewModel.articleBeanList.observe(viewLifecycleOwner) { data ->
            /**
             * Notice: if newList===oldList Nothing will change
             * At first, data list init as a empty list(call as oldList), and then when the data source changed and start notify the livedata observer to change
             * adapter's showing list, we get the new list is the same with old list, so the list will show nothing as we get a empty list
             * see the source code [androidx.recyclerview.widget.ListAdapter.submitList]->[androidx.recyclerview.widget.AsyncListDiffer.submitList]
             * if (newList == mList) {  if (newList == mList) { // nothing to do (Note - still had to inc generation, since may have ongoing work)
             *
             * Fix: we can use [MutableList.toList] to return a new List , and then we can show list
             * */
            adapter.submitList(data.toList())
        }

        viewModel.loadingStatus.observe(viewLifecycleOwner) {
            when (it) {
                Loading.NONE -> footerAdapter.setNoneStatus()
                Loading.LOADING -> footerAdapter.setLoadingStatus()
                Loading.END -> footerAdapter.setEndStatus()
                Loading.FINISH -> footerAdapter.setFinishStatus()
                else -> return@observe
            }
        }
    }
}
复制代码

4、 界面跳转

HomeFragment通过全局操作跳转到SearchFragment

binding.textInputEditText.setOnClickListener {
            exitTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true).apply {
                duration = resources.getInteger(R.integer.motion_duration_large).toLong()
            }
            reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false).apply {
                duration = resources.getInteger(R.integer.motion_duration_large).toLong()
            }
            findNavController().navigate(R.id.action_global_search)
        }
复制代码

从搜索界面返回到HomeFragment, 列表数据和浏览位置都恢复如初。演示视频点击此处 ,由于RecyclerView的数据和状态都被保存了下来,因此完全感受不到任何异常。

猜你喜欢

转载自juejin.im/post/7035576260809981989