Navigation- 02-fragment Life cycle

[TOC]

0, the References

Android Navigation encounter pits – real project experience

Official literature Navigation

Official literature Fragment

Case Warehouse BeerMusic gitee.com/junwuming/B…

At first the Jetpack Navigation drove me crazy, but then it smelled good

PS: Better to look at the first link first

1.Fragment lifecycle

All pits associated with Navigation have a center. Normally, a Fragment is a View, and the life cycle of the View is the life cycle of the Fragment. However, in Navigation architecture, the life cycle of the Fragment is different from the life cycle of the View. When navigate reaches the new UI, the overridden UI, the View is destroyed, leaving the fragment instance intact and will be re-created when the fragment is resumed. This is the root of evil.

Copyright notice: this article is an original article BY CSDN blogger “zijietiaodong technology team”, under CC 4.0 by-sa copyright agreement. Please attach the link of the original source and this statement. The original link: blog.csdn.net/bytedancete…

The View will be destroyed when the Fragment in the Navigation frame presses the stack, and will be built again when it enters the top of the stack. This leads to a problem that activities don’t have: how do you recover the View’s data? Such as: Common RecyclerView will also be destroyed when the stack, when the user returns, need to rebuild RecyclerView, binding Adapter, so Adapter or list data can not be lost, You need to save it in a ViewModel or Fragment class member variable, otherwise you have to ask the network to load the data again (which is a terrible experience). The fragmentation of Fragment and View can be illustrated by the following 2 life cycle diagrams:

  • NavigationIt was officially given before it showed upFragmentThe life cycle is shown below 🙁Pay attention toonDestroyViewThe place of)

  • whileLIfecycle.NavigationAnd so on components after the official givenFragmentThe life cycle is shown below 🙁PS: Fragment Lifecycle && View Lifecycle)

The Fragment Lifecycle can be divided into Fragment Lifecycle and View Lifecycle. The reason for this is that all fragments in the Navigation frame that are not on top of the stack will be destroyed, i.e. A will jump to B: A will call onDestroyView to destroy its View (Databinding and RecyclerView will be destroyed), but the Fragment itself will exist (its member variables will not be destroyed). For this design, please refer to the Issue of Navigation: Navigation, Saving fragment state, many people rewrite Navigation so that it can save View. At first, Jetpack Navigation drove me crazy, but later on, it was pretty much implemented to replace the official replace mode with Hide and Show. Part of the problems were solved, but AFTER stepping on the pit, I felt that the consequences were endless: potential and unpredictable bugs caused by life cycle, LiveData, etc.

The correct state flow under Navigation should look something like this:

A opens B using action, A goes from onResume to onDestroyView, AND B goes from onAttach to onResume. When B returns to A using the system return key, A goes from onCreateView to onResume. In this process, View A is destroyed and rebuilt. The object instance of View(Binding instance) is different, but the instance of Fragment A is always the same.

In such A scenario, assume that A has A network news list RecyclerView, and RecyclerView is destroyed and rebuilt along with the View. How to save the data, avoid every time to return to A to refresh the data (caused: the last browsing data, location loss, additional network resource consumption), so RecyclerView Adapter data items are very key! Common methods of saving are: 1. Member variables in fragments; 2. ViewModel. Method 2 is very suitable, in the ViewModelScope of the ViewModel through the coroutine request network data, stored in the ViewModel (ViewModel life cycle throughout the Fragment), can save data through LiveData, ordinary variables, Restore data after onViewCreated

2. Pits in the project

The following contents are all from the core of Android Navigation encountered pits – real project experience

Databinding requires onDestroyView to be set to Null.

In the Fragment base class shown below we can see that the View instance ViewBinding binding is used as a class member variable. The member variable life cycle in the JVM runs through the object, so when the Fragment state goes to onDestroyView, the Binding instance is not actively released. Of course we don’t want to keep this for reuse because a new binding is inflate again in onCreateView, so remember to release memory that will not be used for the time being when onDestroyView destroys the View instance: 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()}") }Copy the code

Lifecycle when Databinding encounters an error

LifecycleOwner = viewLifecycleOwner (binding.lifecycleOwner = this); First let’s take a look at a familiar MVVM fragment:

override fun initObserve() {
        viewModel.time.observe(viewLifecycleOwner){
            // binding UI
        }
    }
Copy the code

Fragment: add Observe to ViewModel LiveData on onViewCreated So the viewer lifecycle uses the viewLifecycleOwner instead of the Fragment itself (the View is destroyed and there is no point in refreshing the interface), Similarly, the Databinding instance itself disappears when the binding is destroyed, so there is no need to use the Fragment life cycle at all.

More common is a combination of the following to avoid writing too much view-refresh code in your 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>Copy the code

LifecycleOwner = viewLifecycleOwner Lifecycle is required. Lifecycle is required by Lifecycle. LifecycleOwner = viewLifecycleOwner LifecycleOwner = this means that LiveData will not be unregistered when the View is destroyed

Livedata.removeobservers, in onStateChanged(LifecycleOwner, Lifecycle.Event), which accepts a Lifecycle callback, Checks if the current owner is in a DESTROYED state.

To quote from the article:

This code works fine and seems to be executing as expected. Even the official code is written that way. LeakCanary cannot detect memory leaks. LeakCanary can only detect memory leaks from Activity, Fragment and View instances, and there is no way to analyze common class instances.

The problem arises when databinding encounters an incorrect lifecycle. Without Navigation, the View lifecycle is the same as the Fragment lifecycle, but with Navigation, The life cycles of the two are inconsistent. Let’s look at the ViewDataBinding code that sets the lifecycleOwner.

Add an OnStartListener instance to the lifecycleOwner, because the lifecycleOwner is a fragment and will be unregistered when the fragment is destroyed. However, the View will not be unregistered when it is destroyed. OnStartListener has a reference to the ViewDataBinding, which prevents the system from reclaiming the View when it is destroyed.

The analysis logic is correct, but the result is incorrect, and the View will still be reclaimed because the OnStartListener instance holds a weak reference to the View, and the View will still be reclaimed. This is why LeakCanary did not report an error. The OnStartListener instance, however, is not so lucky, and it is this instance’s failure to reclaim that causes the memory leak.

@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); // This instance holds an instance of ViewDataBinging, albeit a weak reference. } lifecycleOwner.getLifecycle().addObserver(mOnStartListener); // The problem here is that if lifecycle is fragment and the View is destroyed there will be no unregister. } for (WeakListener<? > weakListener : mLocalFieldObservers) { if (weakListener ! = null) { weakListener.setLifecycleOwner(lifecycleOwner); }}}Copy the code

You can take a closer look at the subsequent potholes, and you’ll almost always see them after Navigation, especially ViewPager2, which I won’t go into here.

3. Is Glide’s self-managed life cycle reliable?

Can Android component lifecycle self-management be trusted?

5, when ViewPager2 encounters Navigation

6, ViewPager2 set Adapter caused by the Fragment reconstruction problem

7. What should be paid attention to in manual Fragment management under Navigation framework?

8. Under the auspices of Navigation, Fragment and View are separated, how to divide property?

3. View restoration siteThe sample code

The View destruction and reconstruction mentioned above and the restoration of the site are things we must consider. Fortunately, many views save state, so we just need to restore the data in them. Below demonstrate a ViewModel, LiveData, RecyclerView state preservation example, source code please go to BeerMusic HomeFragment.

1.ArticleAdapterBuilding a list adapter

The ListAdapter can use DiffUtil to avoid full flush of normal Adapter. The ListAdapter does not need to consider full flush, insert flush, or delete flush. Avoid complex logic control as much as possible while ensuring high performance.

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 } } }Copy the code

Adapter.submitlist (data.tolist ()))

2. Build the ViewModel to save the data

ViewModel to store list data, ListViewModel is a general RecyclerView data storage ViewModel, where private val _data = MutableLiveData

>(mutableListOf()) uses LiveData to store observable list data, triggers paging requests when the list is pulled up, adds data to _data when network data arrives, and triggers its refresh.

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 universal RecyclerView ViewModel framework}Copy the code

3. Fragment under the MVVM

HomeFragment home page, onCreatedView add on the list of ViewModel data listening, and RecyclerView add load more sliding listening, trigger ViewModel constantly loading data, LiveData data changes notification home page refresh, Complement each other. Here we use the by autoCleared method to automatically destroy the Adapter when the View is destroyed. There is no need to keep the Adapter), the autoCleared tool uses this code to RecyclerView1.2+ and ConcatAdapter to create and load more loops. I won’t go into details.

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
            }
        }
    }
}
Copy the code

4. Jump to the interface

HomeFragment jumps to SearchFragment via global operations:

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)
        }
Copy the code

When you return to HomeFragment from the search screen, the list data and browsing location are restored as before. Demo video Click here, because the RecyclerView data and state are saved, so completely feel no abnormal.