Android-FragmentPagerAdapter刷新无效的解决方案

最近在重构项目的时候有个地方想要做一个更换FragmentPagerAdapter中的Fragment的功能,按照通常使用ListView的习惯做法,如果你只是更新保存Fragment的List数据,然后调用adapter的notifyDataSetChanged()是不会起作用的(下面会分析原因)。
搜索了下发现此问题普遍存在,多数是说先移除Fragment再notifyDataSetChanged(),因为FragmentPagerAdapter内部会缓存Fragment,但是经测试发现仅仅这样干是不行的。于是经过一番折腾,参考了各种方案之后我整理了一个可行的方案,本文做一个记录,以便后续参考,也方便各位道友参考。

下面来分析一下此问题的主要原因:

这可能是Android一个BUG, 与此问题相关的主要有两个方法:

  • getItemPosition()
  • instantiateItem()

搞清楚这两个方法的作用基本就知道如何解决了,先来看第一个方法
getItemPosition()

这个是在PagerAdapter中的getItemPosition()源码的说明,从它的英文注释我们可以清楚的知道,这个方法的返回值的意思是:如果给定的item的position没有发生改变,那么就返回POSITION_UNCHANGED, 如果给定的item在adapter当中指定位置不再呈现了,那么就返回POSITION_NONE。默认返回的是POSITION_UNCHANGED

OK, 导致这个问题的一个主要原因我们已经知道了,所以,默认我们是要重写这个方法的,不然总是返回POSITION_UNCHANGED,那当然是不会更新的了

其实在使用viewpager包含普通view界面的时候我们应该会经常遇到这个问题的。那么, 这个问题的解决思路就有了:
我们就按照它要求的意思来实现当position发生变化的item我们都返回POSITION_NONE,而position没有发生变化的item我们就返回POSITION_UNCHANGED

那怎么来实现呢,我们简单来想一下,首先我要记录更新之前的每个item对应的position,然后在更新Fragment列表数据之后,我们再把当前的每一个item的position跟之前的去比对一遍,这样我们就能知道到底哪个item的position发生了变化,哪个的position依然没变了。当然前提是比对的item是相同的item, 如果更新之后item都不存在了,那自然要返回POSITION_NONE了。
好,我们这里就简单的思路设想一下,后面我会给出完整代码。

到这里包含普通view的viewpager的adapter刷新问题应该可以解决了,注意,这里很多人的暴力做法是在getItemPosition()当中直接返回POSITION_NONE,这样不是不可以,只不过这样做会默认把所有的view都重新销毁重建,那肯定不是我想要的理想的情况。

接下来再看另一个方法:
instantiateItem()
这个是在FragmentPagerAdapter的源码当中的,可以看到在instantiateItem()方法的内部,它是这样做的:根据tag查找对应的Fragment, 如果找到,那么就通过当前的Transaction进行attach操作,这个fragment就会显示了,如果没有找到呢,就去getItem()从你的Fragment列表中获取一个然后Transaction进行add操作。

所以看到这里就恍然大悟了,为啥我list里面的fragment都换了新的了但就是死活不刷新呢,罪魁祸首就在这里了,只要它能findFragmentByTag找得到那么就不会用你的列表中的fragment, 还是用之前的。

那么,到这里首先想到的就是,我们在更换或者删除列表中对应的Fragment时,同时也要将该Fragment从Transaction当中移除,这样就能够确保在刷新数据时adpater会从我们更新后的list中去获取fragment而不是用之前缓存的。

是不是这样?对不对?嗯,应该是没有问题的,好,想到这里那么我们就可以撸起袖子动手干了,加上前面getItemPosition()的思路,应该是能够解决问题的了。假设你按照前面的思路完善了FragmentPagerAdapter的代码并准备测试(或者你可以直接往下拖查看完整的代码),你会悲剧的发现,在更换某一个fragment的时候是没有问题的,但是在删除某一个fragment时是会出现问题的,会发生crash! 抛出如下异常:
这里写图片描述

这里写图片描述

哎,没办法,江湖就是如此险恶,到处都是坑。。
那么究竟为什么发生crash呢,如果你查看该crash异常栈,我们可以在源码中搜素一下找到:

这里写图片描述
没错,就是在高亮的这一行,如果你按照前面介绍的方法写好FragmentPagerAdapter 运行测试了,你就会发现抛出”Can’t change tag of fragment “的异常,我们可以发现上述的异常是在beginTransaction()之后进行add操作发生的,异常出现的判断条件是fragment.mTag != null &&!tag.equals(fragment.mTag),这里的tag就是add时传入的tag参数, 而mTag是要添加的frgament的tag, 这说明这个fragment之前被添加过,因为下面一行fragment.mTag = tag;我们知道只有添加过的fragment的mTag才不会为null。

那问题肯定是跟tag有关了,我们回到instantiateItem()方法的源码,可以看到不管是add操作还是findFragmentByTag时的tag都是通过一个方法生成的:
这里写图片描述
这里写图片描述
makeFragmentName(), 都是这个方法生成的tag, 而这个方法生成tag的办法是getItemId()和viewId的组合, viewId应该就是我们的fragment的id了,而getItemId():
这里写图片描述
它默认实现就是简单的返回position,所以tag是由fragment的id+position组成的。
那我们来分析一下,删除的时候为啥会出现”Can’t change tag of fragment “的异常,先画个简图:

这里写图片描述
假设初始时我们viewpager当中有4个Fragment分别是A B C D, 那么按照instantiateItem()源码中的tag生成方法,这四个fragment被add之后对应四个fragment中的mTag值应该分别就是:A0、B1、C2、D3(假设就用ABCD代表他们的fragment的id),好,现在我们把B对应的Fragment删除掉(注意此时我们已经按照前面已发现的解决方案实现了的代码):
这里写图片描述
此时列表中只剩下A C D三个Fragment, 那么前面提到过,此时getItemPpsition()方法我们应该做的是A对应的Fragment返回POSITION_UNCHANGED, 因为A的位置没有发生变化,而B(已删除)、 C(移位) 、 D(移位) 三个我们应该返回POSITION_NONE,因此我们的adapter在刷新的时候刷新到第二个位置时会再首先去查找对应tag的Fragment:
这里写图片描述
此时查找的tag是C1,然而找不到,因为C前面add的tag是C2,所以走else, 在else当中就会从我们的列表中去get第1个item,那取到的自然是C,然后对C进行add操作,这时又会生成C对应的tag传入add()方法,但是此时,注意了,生成C的tag的方法生成的结果是C1(fragment的id+当前position),分析到这里你可能发现了,前面我们的C是被add过的,所以之前C的mTag是C2,到了这里add操作时要变成C1了!所以跟着源码走进去自然就符合前面“Can’t change tag of fragment “异常的判断条件fragment.mTag != null &&!tag.equals(fragment.mTag),我们的C之前的mTag不为空并且C1 != C2,所以中标了!

那么解决问题的方法,首先想到的是为每一个Fragment设置一个唯一的tag值,但是mTag在Fragment源码中是protected的,我们是不能改的。。。所以只能去改生成tag的方法makeFragmentName()了,但是一看这个方法又是private的,又不能改。。。。我TMD…CNM..MMP…好吧,再看,因为makeFragmentName()方法用到了getItemId()的返回值,而getItemId()我们是可以重写的,所以那去只能改getItemId()方法了:

@Override
public long getItemId(int position) {
    // return position;
    return 我们自定义的可以确定当前item的唯一值;
}

因为前面提到过getItemId()方法默认返回的是position,所以我们这个方法要修改一下,返回一个唯一的值,一个可以标志这个fragment的唯一值就可以了,这样在删除操作position发生变化之后,C的tag值经过makeFragmentName()生成的结果总是C+uniqueId, 所以应该不会有问题了。

好了,至此所有问题思路解决完毕,贴一下完善FragmentPagerAdapter的完整代码:

/**
 * 加载显示Fragment的ViewPagerAdapter基类
 * 提供可以刷新的方法
 *
 * @author Fly
 * @e-mail 1285760616@qq.com
 * @time 2018/3/22
 */
public class BaseFragmentPagerAdapter extends FragmentPagerAdapter {
    private List<BaseFragment> mFragmentList;
    private FragmentManager mFragmentManager;
    /**下面两个值用来保存Fragment的位置信息,用以判断该位置是否需要更新*/
    private SparseArray<String> mFragmentPositionMap;
    private SparseArray<String> mFragmentPositionMapAfterUpdate;

    public BaseFragmentPagerAdapter(FragmentManager fm, List<BaseFragment> fragments) {
        super(fm);
        mFragmentList = fragments;
        mFragmentManager = fm;
        mFragmentList = fragments;
        mFragmentPositionMap = new SparseArray<>();
        mFragmentPositionMapAfterUpdate = new SparseArray<>();
        setFragmentPositionMap();
        setFragmentPositionMapForUpdate();
    }

    /**
     * 保存更新之前的位置信息,用<hashCode, position>的键值对结构来保存
     */
    private void setFragmentPositionMap() {
        mFragmentPositionMap.clear();
        for (int i = 0; i < mFragmentList.size(); i++) {
            mFragmentPositionMap.put(Long.valueOf(getItemId(i)).intValue(), String.valueOf(i));
        }
    }

    /**
     * 保存更新之后的位置信息,用<hashCode, position>的键值对结构来保存
     */
    private void setFragmentPositionMapForUpdate() {
        mFragmentPositionMapAfterUpdate.clear();
        for (int i = 0; i < mFragmentList.size(); i++) {
            mFragmentPositionMapAfterUpdate.put(Long.valueOf(getItemId(i)).intValue(),  String.valueOf(i));
        }
    }

   /**
    * 在此方法中找到需要更新的位置返回POSITION_NONE,否则返回POSITION_UNCHANGED即可
    */
    @Override
    public int getItemPosition(Object object) {
        int hashCode = object.hashCode();
        //查找object在更新后的列表中的位置
        String position = mFragmentPositionMapAfterUpdate.get(hashCode);
        //更新后的列表中不存在该object的位置了
        if (position == null) {
            return POSITION_NONE;
        } else {
            //如果更新后的列表中存在该object的位置, 查找该object之前的位置并判断位置是否发生了变化
            int size = mFragmentPositionMap.size();
            for (int i = 0; i < size ; i++) {
                int key = mFragmentPositionMap.keyAt(i);
                if (key == hashCode) {
                    String index = mFragmentPositionMap.get(key);
                    if (position.equals(index)) {
                        //位置没变依然返回POSITION_UNCHANGED
                        return POSITION_UNCHANGED;
                    } else {
                        //位置变了
                        return POSITION_NONE;
                    }
                }
            }
        }
        return POSITION_UNCHANGED;
    }

    /**
     * 将指定的Fragment替换/更新为新的Fragment
     * @param oldFragment 旧Fragment
     * @param newFragment 新Fragment
     */
    public void replaceFragment(BaseFragment oldFragment, BaseFragment newFragment) {
        int position = mFragmentList.indexOf(oldFragment);
        if (position == -1) {
            return;
        }
        //从Transaction移除旧的Fragment
        removeFragmentInternal(oldFragment);
        //替换List中对应的Fragment
        mFragmentList.set(position, newFragment);
        //刷新Adapter
        notifyItemChanged();
    }

    /**
     * 将指定位置的Fragment替换/更新为新的Fragment,同{@link #replaceFragment(BaseFragment oldFragment, BaseFragment newFragment)}
     * @param position    旧Fragment的位置
     * @param newFragment 新Fragment
     */
    public void replaceFragment(int position, BaseFragment newFragment) {
        BaseFragment oldFragment = mFragmentList.get(position);
        removeFragmentInternal(oldFragment);
        mFragmentList.set(position, newFragment);
        notifyItemChanged();
    }

    /**
     * 移除指定的Fragment
     * @param fragment 目标Fragment
     */
    public void removeFragment(BaseFragment fragment) {
        //先从List中移除
        mFragmentList.remove(fragment);
        //然后从Transaction移除
        removeFragmentInternal(fragment);
        //最后刷新Adapter
        notifyItemChanged();
    }

    /**
     * 移除指定位置的Fragment,同 {@link #removeFragment(BaseFragment fragment)}
     * @param position
     */
    public void removeFragment(int position) {
        BaseFragment fragment = mFragmentList.get(position);
        //然后从List中移除
        mFragmentList.remove(fragment);
        //先从Transaction移除
        removeFragmentInternal(fragment);
        //最后刷新Adapter
        notifyItemChanged();
    }

    /**
     * 添加Fragment
     * @param fragment 目标Fragment
     */
    public void addFragment(BaseFragment fragment) {
        mFragmentList.add(fragment);
        notifyItemChanged();
    }

    /**
     * 在指定位置插入一个Fragment
     * @param position 插入位置
     * @param fragment 目标Fragment
     */
    public void insertFragment(int position, BaseFragment fragment) {
        mFragmentList.add(position, fragment);
        notifyItemChanged();
    }

    private void notifyItemChanged() {
        //刷新之前重新收集位置信息
        setFragmentPositionMapForUpdate();
        notifyDataSetChanged();
        setFragmentPositionMap();
    }

    /**
     * 从Transaction移除Fragment
     * @param fragment 目标Fragment
     */
    private void removeFragmentInternal(BaseFragment fragment) {
        FragmentTransaction transaction = mFragmentManager.beginTransaction();
        transaction.remove(fragment);
        transaction.commitNow();
    }

    /**
     * 此方法不用position做返回值即可破解fragment tag异常的错误
     */
    @Override
    public long getItemId(int position) {
        // 获取当前数据的hashCode,其实这里不用hashCode用自定义的可以关联当前Item对象的唯一值也可以,只要不是直接返回position
        return mFragmentList.get(position).hashCode();
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public List<BaseFragment> getFragments() {
        return mFragmentList;
    }
}

好了,现在这个类可以用来实现Fragment列表中的Fragment替换、删除、添加等操作了,并且可以实时刷新adapter, 你可以试验一下。

测试代码:
Activity代码

public class TestActivity extends FragmentActivity implements View.OnClickListener {
    List<Fragment> mFragmentList;
    ViewPager mViewPager;
    public BaseFragmentPagerAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mViewPager = findViewById(R.id.vp);
        findViewById(R.id.btn_change).setOnClickListener(this);

        mFragmentList = new ArrayList<>();
        mFragmentList.add(getFg("AAA"));
        mFragmentList.add(getFg("BBB"));
        mFragmentList.add(getFg("CCC"));
        mFragmentList.add(getFg("DDD"));
        mAdapter = new BaseFragmentPagerAdapter(getSupportFragmentManager(), mFragmentList);
        mViewPager.setAdapter(mAdapter);
    }

    private TestFragment getFg(String a){
        TestFragment fragment = new TestFragment();
        fragment.setTest(a);
        return fragment;
    }

    @Override
    public void onClick(View view) {
        TestFragment eee = getFg("EEE");

        //新增
        mAdapter.addFragment(eee);
        //插入
        mAdapter.insertFragment(1, eee);

        //删除
        mAdapter.removeFragment(1);
        //删除
        mAdapter.removeFragment(mFragmentList.get(1));

        //替换
        mAdapter.replaceFragment(1, eee);
        //替换
        mAdapter.replaceFragment(mFragmentList.get(0), eee);
    }
}

用到的TestFragment:

public class TestFragment extends Fragment {
    private final static String TAG = TestFragment.class.getSimpleName();
    private String test;
    public View mContentView;

    public void setTest(String test) {
        this.test = test;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e(TAG, "onCreate:  test = "+test);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mContentView = inflater.inflate(R.layout.layout_fg, null);
        Log.e(TAG, "onCreateView: test = "+test);
        return mContentView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Log.e(TAG, "onViewCreated: test = "+test);
        TextView testText = mContentView.findViewById(R.id.tv_test);
        testText.setText(test);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.e(TAG, "onActivityCreated: test = "+test);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e(TAG, "onDestroy:  test = "+test);
    }

}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
    >

    <android.support.v4.view.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_gravity="center"
        />
    <Button
        android:id="@+id/btn_change"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="btn_change"
        />
</LinearLayout>

布局文件很简单就是一个viewpager+一个button, 然后我们在Activity当中点击这个button对vp的adapter所使用的fragment列表进行操作,并观察变化。

注意,封装的Adapter类提供了新增、插入、删除、替换几种方法的重载,可以通过指定的位置或者fragment进行操作,在onClick()中测试时,注释其他的情况,只测试一种情况即可。

另外,我们在TestFragment中的生命周期方法中添加了Log日志,以便观察结果。

运行代码测试你会发现,当替换掉列表中的一个Fragment时,左右两边的Fragment生命周期是不会被调用的。这符合我们的预期,因为替换时左右两边的Fragment在viewpager中的位置没有发生变化,所以它们不会被销毁重建。

当你删除或者插入一个Fragment时,当前Fragment后面的Fragment会走重新创建view的生命周期方法,而当前Fragment前面的Fragment不会,这也符合我们的预期,但为啥后面的会重建,而前面的不会?别忘了我们使用的viewpager是有默认预加载当前页面左右两边的view的特性的,所以这个也属于正常的现象,如果viewpager预加载给你造成了困扰,我们可以通过其它方式来避免,当然这是另外的话题了。

参考:
我在寻找解决方案的过程中参考了这个:remove-fragment-page-from-viewpager-in-android

  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值