在运用Fragment和ViewPager2时遇到了一个古怪的bug,所以顺藤摸瓜学习了一下Fragment和View的状况保存康复流程,解决办法在最后边,对源码解析不感兴趣的可以直接前往

首要看一下溃散调用栈

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)
接下来描绘一下我遇到这个bug的场景,方便大家对号入座:

首要在创立Activity时将MainFragment增加到了Activity中,MainFragment里又会经过FragmentStateAdapter将Fragment增加到MainFragment的ViewPager2中。然后经过消息推送,让activity调用FragmentManager.FragmentTransaction.replace()移除了MainFragment并增加了SecondFragment(这儿还有一行重点代码FragmentManager.FragmentTransaction.addToBackStack(),后边会讲它为什么会导致这个bug的呈现),接着再调用同一个FragmentManager的FragmentManager.popBackStack()办法,然后程序溃散。

然后是排查进程:

首要发现是由于MainFragment只调用了onDestroyView()而没有调用onDestroy()(只毁掉了视图,可是实例还存在),而我的FragmentStateAdapter是跟从MainFragment目标一同初始化的,由于目标没有被毁掉所以只初始化了一次,并且里面的状况(adapter办理的saveStates和fragments也都保存着),所以在Fragment.performActivityCreated时会判断

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

然后会调用到viewpager2的dispatchRestoreInstanceState(),内部最终调用FragmentStateAdapter.restoreState()

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

那么肉眼可见的是,这个bug是和fragment的状况毁掉和重建有关的,大概的原因是:在运用FragmentManager.replace()切换fragment时,FragmentManager会将当时将要被毁掉的Fragment视图从Activity中移除,并将新的Fragment的视图加载到activity上。由于咱们将这个业务参加了回来栈FragmentManager.FragmentTransaction.addToBackStack(),所以FragmentManager不会毁掉或者解绑这个fragment实例,仅仅把视图毁掉了。并且FragmentManager会保存Fragment和Adapter的状况再毁掉视图,在这个业务弹出回来栈时,FragmentManager又会操控fragment康复它的视图状况,接着FragmentStateAdapter发现它自己不干净(mSavedStates不为空),所以自爆了。

接下来详细跟一遍fragment和viewpager2状况保存康复的流程(已简化)

这段的流程有点长,其实大概流程上面已经讲清楚了,所以这儿不看也行(直接看我的吐槽),仅仅看了的话会对理解Fragment和View的状况保存康复流程更明晰

graph TD
a["FragmentStateManager.moveToExpectedState()"]-. "mFragment.mView != null 并且 mFragment.mSavedViewState == null" .-> b["FragmentStateManager.saveViewState()"] --> |"直接调用mFragment.mView的saveHierarchyState办法"|c["View.saveHierarchyState()"] --> 
d["View.dispatchSaveInstanceState()"] -. "id不为空 并且 isSaveEnabled == true" .->
e["ViewPager2.onSaveInstanceState()"] -. "mPendingAdapterState != null" .-> 
f["FragmentStateAdapter.saveState()"]

当我点击/执行了回来操作,触发了FragmentManager.popBackStack(),就会走一遍下面这个流程

graph TD
a["FragmentStateManager.moveToExpectedState()"] --> b["FragmentStateManager.activityCreated()"] --> c["Fragment.performActivityCreated()"] --> 
d["Fragment.restoreViewState()"] -. "mView != null 并且之前调用FragmentStateManager.saveViewState()
用来保存状况的mSavedViewState不为空,
调用mView的restoreHierarchyState办法" .-> 
e["View.restoreHierarchyState()"] --> 
f["View.dispatchRestoreInstanceState()"] -- "在这中心会先调用到ViewPager2.onRestoreInstanceState()
为ViewPager2.mPendingAdapterState和mPendingCurrentItem赋上之前
调用FragmentStateAdapter.saveState()保存的状况" --> 
g["ViewPager2.restorePendingState()"] -. "刚刚赋值的mPendingCurrentItem和mPendingAdapterState的值有用
并且mAdapter不为空" .-> 
h["FragmentStateAdapter.restoreState()"]

在FragmentStateAdapter准备康复当时Fragment视图上的ViewPager2的状况时,溃散就产生了。

一点牢骚

说实话,我觉得官方代码在这儿直接抛出反常是很愚蠢的行为,由于经过将Transaction参加回来栈addToBackStack(),参加回来栈的Fragment就只会被毁掉视图onDestroyView()而实例依然被FragmentManager持有(fragment不会与activity解绑,也不会执行onDestroy()),并将在弹出回来栈时康复这个Fragment的状况,所以如果你不做任何特别处理,FragmentStateAdapter.mSavedStates必然是不为空的,并且FragmentStateAdapter并没有提供任何办法让咱们可以去铲除它的缓存(咱们乃至都不能重写它的saveState()和restoreState(),太扯淡了),因而看起来就像谷歌让ViewPager2不接受一个复用的adapter。我不明白为什么官方要在这儿选择让程序溃散而不是清空之前的mSavedStates,由于要触发这个溃散只需求一个很常见的场景和代码。

吐槽结束接下来就说一下解决办法吧,由于能改动的当地很有限,所以我觉得下面这几个办法都不是很好,并且有利有弊,可是总归是能解决问题。

解决办法

计划1:

将Transaction的replace改成add和hide,避免了fragment从头创立视图,也就不会触发FragmentStateAdapter.restoreState(),所以溃散的问题就解决了(没有动画的需求用这个办法就行了)。可是经过add和hide,我的mainFragment的渐隐动画没有被触发,mainFragment的视图直接被躲藏了,这样肯定是不能满足我的需求的。

计划2:

既然是视图状况康复的时分溃散的,那我禁用掉viewpager2的状况康复不就可以越过抛出反常的代码了吗?调用view.setSaveEnabled(false)就可以禁用view的状况保存和康复。实践成果证明这是可行的,可是我的Fragment消失转场动画也消失了,并且每次回来时都会回来到position 0。

计划3:

不保存adapter的实例,而是在onViewCreated()里每次都创立一个新的FragmentStateAdapter并赋值给viewpager2.adapter,并且在onDestroyView()里将viewpager2的adapter移除掉viewpager2.adapter = null。这个办法的思路和办法2类似,也是经过手动操控避开viewpager2的状况康复代码。

计划4:

先将MainFragment和SecondFragment都增加到activity中,然后躲藏除了MainFragment以外的其他Fragment

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()

然后在需求展示SecondFragment的时分运用FragmentManager.FragmentTransaction.show(secondFragment)FragmentManager.FragmentTransaction.hide(mainFragment)来切换fragment。
这是我以为最好的解决计划。由于这样即避免了fragment的状况保存和康复流程以及fragment各种创立时的回调代码(提高了功能),也能保证过渡动画的正常运作。不过这个办法也有一个坏处,就是咱们需求注意SecondFragment改写界面(加载布局/动画/改写数据)的时机,由于咱们一开始就将fragment都增加到activity上了,所以fragment会跟从activity走完整个发动的生命周期(例如onCreateView()和onResume()),在切换显现躲藏时SecondFragment只会回调onHiddenChange(isHidden:Boolean)办法,所以咱们要注意在SecondFragment真正准备显现出来的时分再执行对应的界面改写操作

计划5:

把ViewPager2换成ViewPager和FragmentStatePagerAdapter,虽然听起来很扯可是确实有用 ; )

end