从一个bug开始,理解Fragment和ViewPager2的状态恢复流程

在使用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的状态保存恢复流程更清晰

在这里插入图片描述

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

在这里插入图片描述

在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,虽然听起来很扯但是确实有用 ; )

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会为您讲解关于AndroidViewPager和Fragment的使用。 ViewPager和FragmentAndroid中非常常用的组件,他们可以一起使用来实现滑动页面效果。ViewPager是一个可以左右滑动切换页面的布局容器,而Fragment作为ViewPager的子页面,可以在ViewPager中进行动态添加和移除。 下面我们将分别介绍ViewPager和Fragment的使用。 ## ViewPager的使用 ### 1.布局文件 在布局文件中,我们需要使用ViewPager作为容器,将需要滑动切换的页面放入其中。如下所示: ``` <androidx.viewpager.widget.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"/> ``` ### 2.创建Adapter 我们需要创建一个Adapter继承自PagerAdapter,并重写以下方法: ``` public class MyPagerAdapter extends PagerAdapter { private List<Fragment> mFragments; public MyPagerAdapter(List<Fragment> fragments) { mFragments = fragments; } @Override public int getCount() { return mFragments.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view == object; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { Fragment fragment = mFragments.get(position); container.addView(fragment.getView()); return fragment.getView(); } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((View) object); } } ``` ### 3.设置Adapter 在Activity或Fragment中,我们需要创建ViewPager的实例,并设置Adapter。如下所示: ``` ViewPager viewPager = findViewById(R.id.viewPager); List<Fragment> fragments = new ArrayList<>(); fragments.add(new Fragment1()); fragments.add(new Fragment2()); fragments.add(new Fragment3()); MyPagerAdapter adapter = new MyPagerAdapter(fragments); viewPager.setAdapter(adapter); ``` 这样,我们就完成了ViewPager的使用。 ## Fragment的使用 ### 1.创建Fragment 我们需要创建一个继承自Fragment的类,并重写以下方法: ``` public class Fragment1 extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment1, container, false); return view; } } ``` ### 2.布局文件 我们需要在Fragment中添加布局文件,如下所示: ``` <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:text="Fragment1" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> ``` 这样,我们就完成了Fragment的使用。 ## ViewPager和Fragment的结合使用 通过以上介绍,我们已经知道了如何使用ViewPager和Fragment了。现在我们需要将它们结合起来使用。 ### 1.创建Fragment 我们需要创建多个Fragment作为ViewPager的子页面。 ### 2.创建Adapter 我们需要创建一个PagerAdapter,将Fragment添加到ViewPager中。如上所示,我们已经创建了一个MyPagerAdapter。 ### 3.设置Adapter 在Activity或Fragment中,我们需要创建ViewPager的实例,并设置Adapter。如上所示,我们已经使用ViewPager的setAdapter方法设置了MyPagerAdapter。 这样,我们就完成了ViewPager和Fragment的结合使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值