在运用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