一、前言
在最近使用ViewPager2显示多个Fragment的时候遇到一个问题,就是我在删除某个fragment的时候发现更新适配器后页面并没有和如期的一样删除这个fragment,看下到底发生了什么?
二、问题剖析
一般我们刷新页面常用的方法是调用适配器的notifyDataSetChanged
方法,我们先看下ViewPager2的适配器更新方法都干啥了?
public class RecyclerView{
public abstract static class Adapter<VH extends ViewHolder> {
...这次省略部分代码
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
}
}
可以看出,更新调用的是RecycleView的适配器方法,所以从这里我们也可以很直观的知道,ViewPager2跟ViewPager不一样的是使用的适配器不一样,ViewPager2是基于RecycleView的适配器策略基础上实现的,在这里就不细说了。然后接着查看源码,这里就贴一些关键的代码:
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
private void ensureFragment(int position) {
//通过下标获取当前项的id
long itemId = getItemId(position);
if (!mFragments.containsKey(itemId)) {
// TODO(133419201): check if a Fragment provided here is a new Fragment
Fragment newFragment = createFragment(position);
newFragment.setInitialSavedState(mSavedStates.get(itemId));
//保存Fragment
mFragments.put(itemId, newFragment);
}
}
先看这个方法,从上面可以看出,适配器内部用LongSparseArray
变量存储Fragment,存入Fragment调用的是ensureFragment
方法,从方法上面可以看到,getItemId
作为key,createFragment
作为value存入到mFragments
当中,也就是说mFragments
通过键值对的形式存储Fragment,此后读取fragment,它的key是一个关键,那么看下它的key是怎么生成的?
public long getItemId(int position) {
return position;
}
可以看出默认key就是当前item的下标。那么现在可以猜测到不刷新的原因可能是因为下标没有改变,所以新的fragment是存不进去的,且通过key,也就是下标获取到的fragment还是之前那一个fragment。为了验证我们的猜想,我们再看下一个方法updateFragmentMaxLifecycle
。
void updateFragmentMaxLifecycle(boolean dataSetChanged) {
...此处省略部分代码
//获取当前项的下标
final int currentItem = mViewPager.getCurrentItem();
if (currentItem >= getItemCount()) {
/** current item is yet to be updated; it is guaranteed to change, so we will be
* notified via {@link ViewPager2.OnPageChangeCallback#onPageSelected(int)} */
return;
}
//获取当前项的id,如果上一次的
long currentItemId = getItemId(currentItem);
//如果获取的key还是之前那一个,则直接返回
if (currentItemId == mPrimaryItemId && !dataSetChanged) {
return; // nothing to do
}
//通过id获取当前的Fragment
Fragment currentItemFragment = mFragments.get(currentItemId);
if (currentItemFragment == null || !currentItemFragment.isAdded()) {
return;
}
//保存当前项的id
mPrimaryItemId = currentItemId;
FragmentTransaction transaction = mFragmentManager.beginTransaction();
Fragment toResume = null;
for (int ix = 0; ix < mFragments.size(); ix++) {
long itemId = mFragments.keyAt(ix);
Fragment fragment = mFragments.valueAt(ix);
if (!fragment.isAdded()) {
continue;
}
if (itemId != mPrimaryItemId) {
transaction.setMaxLifecycle(fragment, STARTED);
} else {
toResume = fragment; // itemId map key, so only one can match the predicate
}
fragment.setMenuVisibility(itemId == mPrimaryItemId);
}
if (toResume != null) { // in case the Fragment wasn't added yet
transaction.setMaxLifecycle(toResume, RESUMED);
}
if (!transaction.isEmpty()) {
transaction.commitNow();
}
}
从这个以看出,果然,首先获取当前的下标,如果当前的下标没有改变,则直接返回,这个判断验证了我之前删除第一个Fragment后为什么不刷新,它直接return了。再看下面直接对mFragments进行处理,到这里想必答案已经有了。所有的问题源头就是这个key,如果我们把生成key的规则改一下,让它不会像下标一样容易重复,那么问题解决了。
三、问题解决
其实只要保证生成的ItemId不重复就行,在这里我直接用fragment的hashCode值,到这里问题就解决了。
public class SimpleChipViewPage2Adapter extends FragmentStateAdapter {
private List<Fragment> fragments;
public SimpleChipViewPage2Adapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, List<Fragment> fragments) {
super(fragmentManager, lifecycle);
this.fragments = fragments;
}
@NonNull
@Override
public Fragment createFragment(int position) {
return fragments == null ? null : fragments.get(position);
}
@Override
public int getItemCount() {
return fragments == null ? 0 : fragments.size();
}
//重载getItemId方法
@Override
public long getItemId(int position) {
return fragments == null ? 0 : fragments.get(position).hashCode();
}
}