FragmentStateAdapter在activity重建时触发设计假设违例报错的解决
提问内容
场景描述
在Android开发中,开发者使用ViewPager2结合FragmentStateAdapter实现多页面滑动切换功能,在适配器承载的某一个Fragment中为用户提供了触发活动重建的入口,内部调用activity.recreate()方法实现重建逻辑。但当活动被重建完成后,应用会抛出异常。
异常信息
java.lang.IllegalStateException: Design assumption violated. at androidx.viewpager2.adapter.FragmentStateAdapter.placeFragmentInViewHolder(FragmentStateAdapter.java:287) at androidx.viewpager2.adapter.FragmentStateAdapter.onViewAttachedToWindow(FragmentStateAdapter.java:276) at androidx.viewpager2.adapter.FragmentStateAdapter.onViewAttachedToWindow(FragmentStateAdapter.java:67) at androidx.recyclerview.widget.RecyclerView.dispatchChildAttached(RecyclerView.java:7556) at androidx.recyclerview.widget.RecyclerView$5.addView(RecyclerView.java:860) at androidx.recyclerview.widget.ChildHelper.addView(ChildHelper.java:107) at androidx.recyclerview.widget.RecyclerView$LayoutManager.addViewInt(RecyclerView.java:8601)
现有实现代码
FragmentStateAdapter 实现
class MainViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { var fragmentList: ArrayList<Fragment> = arrayListOf() override fun getItemCount(): Int = fragmentList.size // 根据位置返回对应需要展示的Fragment override fun createFragment(position: Int): Fragment { return fragmentList[position] } fun add(fragment: Fragment) { fragmentList.add(fragment) notifyDataSetChanged() } fun addByPosition(position: Int, fragment: Fragment) { fragmentList.add(position, fragment) notifyDataSetChanged() } fun remove(index: Int) { fragmentList.removeAt(index) notifyDataSetChanged() } override fun getItemId(position: Int): Long { return fragmentList[position].hashCode().toLong() } override fun containsItem(itemId: Long): Boolean { return fragmentList.contains(itemId) }}
依赖版本
implementation "androidx.viewpager2:viewpager2:1.0.0"
已尝试方案
- 移除
getItemId()的重写逻辑:该操作虽然能让崩溃消失,但无法满足适配器增删Fragment时维持各元素正确相对位置的业务需求; - 尝试在
onSaveInstanceState(outState: Bundle)中保存Fragment列表,在活动重建时将列表回传给适配器,但仍会触发相同的崩溃。
待解决问题
目前尚未定位到代码存在的问题,也希望了解该场景下的最优解决方案。
问题解答
崩溃根因
本次问题由多重违反FragmentStateAdapter设计规范的实现叠加导致:
- 直接持有外部Fragment实例违反设计原则
FragmentStateAdapter的核心逻辑是自行管理Fragment的创建、缓存、状态保存与恢复,明确禁止外部传入已创建的Fragment实例。活动重建时,系统会自动从之前保存的状态中恢复Adapter内部缓存的Fragment实例,但当前实现的createFragment()会直接返回手动存入列表的全新Fragment实例,二者不匹配就会触发placeFragmentInViewHolder方法中的校验逻辑,抛出Design assumption violated异常。 - itemId相关方法重写完全错误
getItemId()使用Fragment的hashCode作为返回值:hashCode是实例内存地址的派生值,活动重建后所有Fragment实例都会重新创建,hashCode完全变化,无法作为跨重建的稳定标识。containsItem()逻辑错误:itemId是Long类型,直接和ArrayList<Fragment>做匹配,本质是用Long对象和Fragment实例对比,永远返回false,直接破坏了Adapter内部的状态匹配逻辑。
- 依赖版本过旧
viewpager2:1.0.0是首个正式版,存在大量状态恢复相关的已知兼容性bug,放大了实现不规范带来的问题。
已尝试方案失效原因
- 移除
getItemId()重写后,Adapter会默认使用position作为itemId,关闭了Fragment实例的匹配校验,因此崩溃消失,但增删页面时会出现Fragment状态错位,无法满足业务需求。 - 手动保存Fragment列表回传给Adapter的操作,和系统自动的Fragment状态恢复逻辑冲突,重建后会存在两份完全不同的Fragment实例,自然无法匹配。
最优落地实现
按照ViewPager2官方规范重构Adapter,同时满足增删页面的位置稳定性需求,具体步骤:
- 替换数据源为页面配置而非Fragment实例
不再持有ArrayList<Fragment>,改为存储业务侧的页面配置信息,保证配置可以跨重建序列化:data class PageConfig( val stablePageId: Long, // 业务侧分配的全局唯一稳定id,重建前后不变 val fragmentClass: Class<out Fragment>, val args: Bundle? = null ) - 正确重写itemId相关方法,提供稳定标识:
override fun getItemId(position: Int): Long { // 返回稳定的业务id,而非Fragment实例的hashCode return pageConfigList[position].stablePageId } override fun containsItem(itemId: Long): Boolean { // 正确匹配itemId与页面配置的id return pageConfigList.any { it.stablePageId == itemId } } - 重写
createFragment()逻辑,按需新建Fragment:override fun createFragment(position: Int): Fragment { val config = pageConfigList[position] // 新建Fragment实例,传入预设参数 return config.fragmentClass.getDeclaredConstructor().newInstance().apply { arguments = config.args } } - 增删页面逻辑同步修改:操作
pageConfigList后调用notifyDataSetChanged()即可,新增页面时分配全局唯一的stablePageId,即可保证增删时各页面的状态和相对位置稳定。 - 状态保存逻辑修改:仅需要在Activity的
onSaveInstanceState中序列化pageConfigList,重建时恢复后传入Adapter即可,不需要手动保存/恢复Fragment实例。 - 升级依赖到最新稳定版,修复历史兼容性问题:
implementation "androidx.viewpager2:viewpager2:1.1.0"
内容的提问来源于stack exchange,提问作者user3182266
相关产品推荐
相关产品推荐

