Starting from a bug, understand the state restoration process of Fragment and ViewPager2

I encountered a strange bug when using Fragment and ViewPager2, so I learned about the state preservation and recovery process of Fragment and View, and the solution is at the end.
First look at the crash call stack

java.lang.IllegalStateException: Expected the adapter to be 'fresh' while restoring state.
at androidx.viewpager2.adapter.FragmentStateAdapter.restoreState(FragmentStateAdapter.java:536)
at androidx.viewpager2.widget.ViewPager2.restorePendingState(ViewPager2.java:350)
at androidx.viewpager2.widget.ViewPager2.dispatchRestoreInstanceState(ViewPager2.java:375)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:4099)
at android.view.View.restoreHierarchyState(View.java:20357)
at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:639)
at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:3010)
at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:3001)

Next, I will describe the scene where I encountered this bug, so that everyone can check in:

First, add MainFragment to Activity when creating Activity, and then add Fragment to ViewPager2 of MainFragment through FragmentStateAdapter in MainFragment. Then through the message push, let the activity call to FragmentManager.FragmentTransaction.replace()remove the MainFragment and add the SecondFragment (here is another line of key code FragmentManager.FragmentTransaction.addToBackStack(), why it will cause this bug to appear later), and then call the same FragmentManager FragmentManager.popBackStack()method, and then the program crashes.

Then there is the troubleshooting process:

The first thing I found out was that MainFragment only called onDestroyView() but not onDestroy() (only the view was destroyed, but the instance still exists), and my FragmentStateAdapter was initialized along with the MainFragment object, because the object was not destroyed so it was only initialized Once, and the state inside (the saveStates and fragments managed by the adapter are also saved), so it will be judged when Fragment.performActivityCreated

if (mView != null) {
    
    
    restoreViewState(mSavedFragmentState);
}

Then it will call dispatchRestoreInstanceState() of viewpager2, and finally call FragmentStateAdapter.restoreState() internally

if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) {
    
    
    throw new IllegalStateException(
            "Expected the adapter to be 'fresh' while restoring state.");
}

Well, it is visible to the naked eye that this bug is related to the state destruction and reconstruction of the fragment. The approximate reason is: when switching FragmentManager.replace()fragments, the FragmentManager will remove the Fragment view that is currently about to be destroyed from the Activity, and put the new one The Fragment's view is loaded on the activity. Because we added this transaction to the return stack FragmentManager.FragmentTransaction.addToBackStack(), FragmentManager will not destroy or unbind the fragment instance, but just destroy the view. And FragmentManager will save the state of Fragment and Adapter and then destroy the view. When the transaction pops up and returns to the stack, FragmentManager will control the fragment to restore its view state, and then FragmentStateAdapter finds that it is not clean (mSavedStates is not empty), so it explodes.

Next, follow the fragment and viewpager2 state saving and restoration process in detail (simplified)

This section of the process is a bit long. In fact, the process has probably been explained clearly above. Just reading it will make it clearer to understand the state preservation and recovery process of Fragment and View.

insert image description here

When I click/execute the return operation and trigger it FragmentManager.popBackStack(), I will go through the following process

insert image description here

When the FragmentStateAdapter is about to restore the state of ViewPager2 on the current Fragment view, the crash occurs.

little whining

To be honest, I think it is stupid for the official code to throw an exception directly here, because by adding Transaction to the return stack addToBackStack(), the Fragment added to the return stack will only be destroyed onDestroyView()and the instance will still be held by FragmentManager (fragment will not be associated with Activity is unbound, and onDestroy()) will not be executed , and the state of this Fragment will be restored when the return stack is popped, so if you do not do any special processing, FragmentStateAdapter.mSavedStatesit must not be empty, and FragmentStateAdapter does not provide any method for We can clear its cache (we can't even override its saveState() and restoreState(), that's bullshit), so it looks like Google made ViewPager2 not accept a reused adapter. I don't understand why the official chooses to let the program crash here instead of clearing the previous mSavedStates, because it only needs a very common scene and code to trigger this crash.

After complaining, let's talk about the solution, because the places that can be changed are very limited, so I think the following methods are not very good, and there are advantages and disadvantages, but they can solve the problem in the end.

Solution

plan 1:

Change the replace of Transaction to add and hide, which avoids fragment recreating the view, and will not trigger FragmentStateAdapter.restoreState(), so the crash problem is solved (this method is fine if there is no need for animation). But through add and hide, the fading animation of my mainFragment is not triggered, and the view of mainFragment is directly hidden, which certainly cannot meet my needs.

Scenario 2:

Since it crashed when the view state was restored, can I skip the code that throws the exception if I disable the state restoration of viewpager2? Call view.setSaveEnabled(false) to disable view state saving and restoration. The practice results have proved that this is feasible, but my Fragment disappears and the transition animation also disappears, and it will return to position 0 every time it returns.

Option 3:

Instead of saving the instance of the adapter, onViewCreated()a new FragmentStateAdapter is created every time in it and assigned to viewpager2.adapter, and onDestroyView()the adapter of viewpager2 is removed in it viewpager2.adapter = null. The idea of ​​this method is similar to method 2, and it also avoids the state recovery code of viewpager2 through manual control.

Option 4:

First add both MainFragment and SecondFragment to the activity, and then hide other Fragments except MainFragment

val secondFragment = SecondFragment()
supportFragmentManager.beginTransaction()
    .add(
        vb.container.id,
        MainFragment::class.java,
        null,
        MainFragment::class.simpleName
    )
    .add(
        vb.container.id,
        SecondFragment,
        SecondFragment::class.simpleName
    )
    .hide(pictureDetailsFragment)
    .commit()

FragmentManager.FragmentTransaction.show(secondFragment)Then use and FragmentManager.FragmentTransaction.hide(mainFragment)to switch fragments when you need to display SecondFragment . This is the best solution in my opinion. Because this avoids the state saving and restoration process of the fragment and the callback code when the fragment is created (improving performance), it also ensures the normal operation of the transition animation. However, this method also has a disadvantage, that is, we need to pay attention to the timing of SecondFragment refreshing the interface (loading layout/animation/refreshing data), because we added the fragments to the activity from the beginning, so the fragments will follow the activity through the entire startup process Life cycle ( 例如onCreateView()和onResume()), SecondFragment will only call back the method when switching display and hiding onHiddenChange(isHidden:Boolean), so we should pay attention to perform the corresponding interface refresh operation when SecondFragment is really ready to be displayed

Option 5:

Replaced ViewPager2 with ViewPager and FragmentStatePagerAdapter, sounds silly but works ;)

at last

If you want to become an architect or want to break through the 20-30K salary range, then don't be limited to coding and business, but you must be able to select models, expand, and improve programming thinking. In addition, a good career plan is also very important, and the habit of learning is very important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here I would like to share with you a set of "Advanced Notes on the Eight Major Modules of Android" written by a senior architect of Ali, to help you organize the messy, scattered and fragmented knowledge systematically and efficiently. Master the various knowledge points of Android development.
img
Compared with the fragmented content we usually read, the knowledge points of this note are more systematic, easier to understand and remember, and are arranged strictly according to the knowledge system.

Welcome everyone to support with one click and three links. If you need the information in the article, you can directly scan the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓

PS: There is also a ChatGPT robot in the group, which can answer your work or technical questions

Guess you like

Origin blog.csdn.net/weixin_43440181/article/details/131646447