简述
同事开发过程中,由于需要用到了 Viewpager 组件很简单也没有什么复杂的东西,就使用 Viewpager 加载了几个 Fragment,Fragment 中有个 RecyclerView 里面加载了一些 item,大概就长这么个样子(如下图)。
发现问题
测试在测试过程中,修改了虚拟导航的方式,从手势变成了按键式。然后回到应用,发现 Fragment 中数据丢失了,并且必现。(同事过节跑了,只能转到我手上了)
先看下代码吧
TestKotlinApiActivity.kt
class TestKotlinApiFragment : SupportFragment() {
、、、、
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vpBrowser.run {
adapter = TestAdapter(childFragmentManager, arrayListOf("", "", "", ""))
}
}
}
TestAdapter.kt
class TestAdapter(fm: FragmentManager, val datas: MutableList<String>) :
FragmentStatePagerAdapter(fm) {
private val mFragments = hashMapOf<Int, Fragment>()
override fun getCount(): Int {
return datas.size
}
override fun getItem(position: Int): Fragment {
return mFragments.getOrPut(position) {
Fragment().apply {
setData(datas)
}
}
}
}
查找问题原因
发现系统修改虚拟会导致 Fragment 重建。从日志中分析所有页面都重建了,数据也给到了新的 Fragment ,但是显示出来的 Fragment 就是一个空的,而且没有调用 onStart、onCreateView 等等方法只调用了 onResume ,这里就是一个比较奇怪的点了?
这里就有两个奇怪的点:
- 为什么 Fragment 不走 onstart、onCreateView、onCreatedView 方法,单单走了 onResume?
- 为什么给了数据,列表还是显示为空呢?
第二点比较好解释,由于 Fragment 不走 onstart、onCreateView、onViewCreated 方法导致 adpter 为空了
第一点就比较奇怪了,就算是重建 TestKotlinApiFragment 的生命周期都走了,而且 TestAdapter 也重建了,并且该给的数据也给了子 Fragment 为什么数据显示为空?这个时候单看这几个类已经解释不了了,只能去父类看看源码了
// FragmentStatePagerAdapter.class 部分源码
private ArrayList<Fragment> mFragments = new ArrayList<>();
@NonNull
public abstract Fragment getItem(int position);
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
// 省略部分代码
Fragment fragment = getItem(position);
// 省略部分代码
while (mFragments.size() <= position) {
mFragments.add(null);
}
// 省略部分代码
return fragment;
}
@Override
@Nullable
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i=0; i<mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
@Override
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle)state;
bundle.setClassLoader(loader);
Parcelable[] fss = bundle.getParcelableArray("states");
mSavedState.clear();
mFragments.clear();
if (fss != null) {
for (int i=0; i<fss.length; i++) {
mSavedState.add((Fragment.SavedState)fss[i]);
}
}
Iterable<String> keys = bundle.keySet();
for (String key: keys) {
if (key.startsWith("f")) {
int index = Integer.parseInt(key.substring(1));
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
while (mFragments.size() <= index) {
mFragments.add(null);
}
f.setMenuVisibility(false);
mFragments.set(index, f);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
从 FragmentStatePagerAdapter 源码中其实立马就能找出为什么子 Fragmet 为什么为空了
// FragmentStatePagerAdapter.class 部分源码
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
// 省略部分代码
Fragment fragment = getItem(position);
// 省略部分代码
while (mFragments.size() <= position) {
mFragments.add(null);
}
// 省略部分代码
return fragment;
}
从这段代码可以看出,由于刚开始创建 mFragments 肯定是一个空的列表,那么就会调用 getItem 去获取 Fragment 填充到内部维护的 Fragmet 列表中。这个时候由于修改了虚拟键盘,导致应用重建就会调用 saveState 方法,将 mFragments 的 Fragments 存放到 FragmentManager 中。下面是源码
// FragmentStatePagerAdapter.class 部分源码
@Override
@Nullable
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i=0; i<mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
重建的时候又会调用 restoreState 方法恢复 Fragment。源码如下
// FragmentStatePagerAdapter.class 部分源码
@Override
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle)state;
bundle.setClassLoader(loader);
Parcelable[] fss = bundle.getParcelableArray("states");
mSavedState.clear();
mFragments.clear();
if (fss != null) {
for (int i=0; i<fss.length; i++) {
mSavedState.add((Fragment.SavedState)fss[i]);
}
}
Iterable<String> keys = bundle.keySet();
for (String key: keys) {
if (key.startsWith("f")) {
int index = Integer.parseInt(key.substring(1));
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
while (mFragments.size() <= index) {
mFragments.add(null);
}
f.setMenuVisibility(false);
mFragments.set(index, f);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
从源码中可以看到,恢复的时候首先会将 mFragments.clear() 掉,然后再从 FragmentManager 中获取到之前存储的 Fragment 填充到 mFragents 中。所以这个时候无论是 Adapter 是否是新的,最终还是用的销毁之前存储的 Fragment。
这个时候就会导致外部自己维护的 mFragents 和内部的 mFragents 不一致。而且源码中内部维护的 mFragents 不为空就直接返回了,所以无论你怎么更新自己维护的 mFragents 都没法去显示到屏幕上。
// FragmentStatePagerAdapter.class 部分源码
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
// 省略部分代码
return fragment;
}
解决方案
很简单,既然知道外部的 Fragments 和 内部的 Fragments 不一致,那么就想办法统一嘛。重写 restoreState 方法
override fun restoreState(state: Parcelable?, loader: ClassLoader?) {
if (state != null) {
val bundle = state as Bundle
bundle.classLoader = loader
mFragments.clear()
val keys: Iterable<String> = bundle.keySet()
for (key in keys) {
if (key.startsWith("f")) {
val index = key.substring(1).toInt()
val f = fm.getFragment(bundle, key)
if (f != null) {
f.setMenuVisibility(false)
mFragments[index] = f
}
}
}
}
}
直接照搬源码自己也用存储的 Fragments 这样就能完美运行起来了。
好,到这里 bug 就解决了,当然有更好的方案大佬们可以指点指点。