Navigation-02-Fragment生命周期
[TOC]
0、 References
Android Navigation 遇坑记 - 真实项目经历
案例仓库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
类成员变量中保存, 否则就只能再次请求网络加载数据(体验非常糟糕)。Fragment
和View
分家的问题,用下面的2张生命周期图可以阐释出来:
Navigation
出现之前官方给出的Fragment
生命周期如下图:(注意onDestroyView
之处)
- 而
LIfecycle
,Navigation
等组件出现之后,官方给出的Fragment
生命周期图为下图:(PS:Fragment Lifecycle && View Lifecycle)
Navigation
框架下的Fragment
生命周期分为 Fragment Lifecycle 和 View Lifecycle
,View 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
方式替换为Hide
和Show
。 解决了一部分问题,但是踩过坑的我感觉遗祸无穷: 生命周期、LiveData等导致的潜在、 防不胜防的bug。
Navigation框架之下的正确状态流转应该是类似这的:
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的时候重新刷新数据(造成:上次浏览数据、位置丢失、额外的网络资源消耗), 因此RecyclerView
中Adapter
的数据项非常关键! 常见的保存方式有: 1、通过Fragment
的成员变量 2、ViewModel
。方法2非常合适,在ViewModel
的ViewModelScope
通过协程请求网络数据,保存在ViewModel
(ViewModel
生命周期贯穿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
, 参考前者的写法,后者不难理解了(可以去看一下XML
的DatabindingImpl
文件),前者若设置 binding.lifecycleOwner = this
, 那么在View销毁时候,LiveData是不会反注册的
LiveData.removeObservers(Observer) 其实是在接受生命周期回调的 onStateChanged(LifecycleOwner, Lifecycle.Event) 方法中,会判断如果是如果当前 owner 处于 DESTROYED 状态就执行。
引用文章中的解释:
这段代码运行起来没有问题,看起来都是按照预期的在执行。甚至官方代码也是这么写的。连 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
构建列表适配器
使用ListAdapter
和DiffUtil.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 autoCleared
在View
销毁时自动销毁Adapter
(数据存储在ViewModel
,没必要再保留Adapter
), autoCleared
工具参考这部分代码 , 这里使用ConcatAdapter
组合Adapter
(RecyclerView1.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
的数据和状态都被保存了下来,因此完全感受不到任何异常。