FragmentPagerAdapter调用notifyDataSetChanged无效及解决方案

FragmentPagerAdapter和FragmentStatePagerAdapter的区别

  • FragmentPagerAdapter 继承自 PagerAdapter。相比通用的 PagerAdapter,该类更专注于每一页均为 Fragment 的情况。如文档所述,该类内的每一个生成的 Fragment 都将保存在内存之中,因此适用于那些相对静态的页,数量也比较少的那种;如果需要处理有很多页,并且数据动态性较大、占用内存较多的情况,应该使用FragmentStatePagerAdapter。
  • FragmentStatePagerAdapter 重载实现了几个必须的函数,因此来自 PagerAdapter 的函数,我们只需要实现 getCount(),即可。且,由于 FragmentPagerAdapter.instantiateItem() 的实现中,调用了一个新增的虚函数 getItem(),因此,我们还至少需要实现一个 getItem()。因此,总体上来说,相对于继承自 PagerAdapter,更方便一些。

前言

前段时间遇到了这么一个需求,当某个终端分配多个订单时可以切换订单分别处理,当时想着这不是分分钟的事吗,上面一个Tablayout下面一个ViewpagerTablaout里面的item展示各订单号,Viewpager里面添加相应的Fragment就搞定了。
后来仔细一想每个订单主要有两种状态,一种是待激活,一种是已激活。假设未激活订单需展示fragment1,已激活订单展示fragment2,需要满足在fragment1中激活当前订单,这时候应该将fragment1替换成fragment2

  • 这时候我首先想到的是能否直接替换fragment1,后来一想Replace需要指定Fragment的容器,而我们是直接使用viewpager来装载Fragment的,这样显然是不可行的。
  • 然后想到之前的一种解决方法,就是Viewpager里面添加一个空的Fragment,里面只有一个FrameLayout布局当做容器,然后在这个空的Fragment中往FrameLayout添加、替换子Fragment,这样是能够满足我们需求的。
  • 不过我觉得上一种方法写着不是特别舒服(其实就是没事找事),想着既然都是使用了Adapter,能否像LIstViewRecyclerView一样通过notifyDataSetChanged方法来更新呢,然后便有了踩坑之旅…

准备

首先我们新建一个项目,简单的创建一个MainActivity,对应的XML布局如下:

"1.0" encoding="utf-8"?>
.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    .support.design.widget.TabLayout
        android:id="@+id/mTabLayout"
        android:layout_width="match_parent"
        android:layout_height="50dp"/>


    .support.v4.view.ViewPager
        android:id="@+id/mViewPager"
        app:layout_constraintTop_toBottomOf="@+id/mTabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    .support.v4.view.ViewPager>

.support.constraint.ConstraintLayout>

然后为了方便查看Fragment的生命周期情况,我们新建一个Fragment的基类BaseFragment,代码如下:

public class BaseFragment extends Fragment {

    protected final String TAG = "======"+this.getClass().getSimpleName();


    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d(TAG,"isVisibleToUser:"+isVisibleToUser);
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.d(TAG,"onSaveInstanceState");
    }

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);
        Log.d(TAG,"onViewStateRestored");
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(TAG,"onAttach");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG,"onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.d(TAG,"onCreateView");
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG,"onActivityCreated");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG,"onStart");
    }


    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG,"onResume");
    }


    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG,"onPause");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(TAG,"onStop");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG,"onDestroyView");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"onDestroy");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG,"onDetach");
    }

只是简单的用log输出了一下各方法的执行情况,然后我们新建几个Fragment继承该基类,定义了一个接口用于在MainActivity执行回调方法,代码如下:

Fragment1

public class Fragment1 extends BaseFragment {


    @BindView(R.id.bt_replace)
    Button btReplace;
    Unbinder unbinder;

    private IReplaceCurrentFg iReplaceCurrentFg;

    public Fragment1() {
        // Required empty public constructor
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        iReplaceCurrentFg = (IReplaceCurrentFg) context;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        super.onCreateView(null, container, savedInstanceState);
        View view = inflater.inflate(R.layout.fragment_fragment1, container, false);
        unbinder = ButterKnife.bind(this, view);
        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        unbinder.unbind();
    }

    @OnClick(R.id.bt_replace)
    public void onViewClicked() {
        if (iReplaceCurrentFg != null){
            iReplaceCurrentFg.replace();
        }
    }

    public interface IReplaceCurrentFg{
        void replace();
    }
}

对应的xml很简单,只有一个Button用于触发回调方法,代码如下:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             tools:context=".Fragment1">

    <Button
        android:id="@+id/bt_replace"
        android:layout_gravity="center"
        android:text="替换当前fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <android.support.design.widget.TextInputEditText
        tools:text="1634153"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

FrameLayout>

然后我们新建一个ReplaceFragment用来展示替换后的新内容,布局和代码如下:


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             tools:context=".ReplaceFragment">



    <Button
        android:id="@+id/bt_add"
        android:text="添加Fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/bt_addChild"
        android:text="添加Edit"
        android:layout_marginLeft="200dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:id="@+id/container"
        android:layout_marginTop="100dp"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">


    LinearLayout>



    
    <TextView
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是替换后的fragment"/>

FrameLayout>

ReplaceFragment

public class ReplaceFragment extends BaseFragment {


    @BindView(R.id.bt_add)
    Button btAdd;
    Unbinder unbinder;
    @BindView(R.id.container)
    LinearLayout container;
    @BindView(R.id.bt_addChild)
    Button btAddChild;

    private IAddNewFragment iAddNewFragment;

    public ReplaceFragment() {
        // Required empty public constructor
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);

    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        iAddNewFragment = (IAddNewFragment) context;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.fragment_replace, container, false);
        unbinder = ButterKnife.bind(this, view);
        return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        btAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (iAddNewFragment != null) {
                    iAddNewFragment.add();
                }
            }
        });

        btAddChild.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                EditText editText = new EditText(getContext());
                container.addView(editText);
            }
        });
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        unbinder.unbind();
    }

    public interface IAddNewFragment {
        void add();
    }
}

使用FragmentPagerAdapter

然后我们在MainActivity中将ViewpagerTabLayout,添加几条静态数据


        mFragmentList = new ArrayList<>();
        mTitleList = new ArrayList<>();

        Fragment1 fragment1 = new Fragment1();
        Fragment2 fragment2 = new Fragment2();
        Fragment3 fragment3 = new Fragment3();
        Fragment4 fragment4 = new Fragment4();

        mFragmentList.add(fragment1);
        mFragmentList.add(fragment2);
        mFragmentList.add(fragment3);
        mFragmentList.add(fragment4);

        mTitleList.add("标题1");
        mTitleList.add("标题2");
        mTitleList.add("标题3");
        mTitleList.add("标题4");

        mFgPagerAdapter = new MyFgPagerAdapter(getSupportFragmentManager(), mFragmentList, mTitleList);
        mViewPager.setAdapter(mFgPagerAdapter);

        mTabLayout.setupWithViewPager(mViewPager);

MyFgPagerAdapter是继承于FragmentPagerAdapter的,代码如下:

public class MyFgPagerAdapter extends FragmentPagerAdapter {

    private FragmentManager fm;
    private List fragmentList;
    private List titleList;


    public MyFgPagerAdapter(FragmentManager fm, List fragmentList, List titleList) {
        super(fm);
        this.fm = fm;
        this.fragmentList = fragmentList;
        this.titleList = titleList;
    }

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

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

    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        return titleList.get(position);
    }
}

项目运行效果如下
FragmentPagerAdapter调用notifyDataSetChanged无效及解决方案_第1张图片
我们可以看见标题1对应的Fragment中有个按钮,我们点击这个按钮来将Fragment1替换成ReplaceFragment,button对应的点击事件如下:

  public void replace() {
        //默认replace第一个
        ReplaceFragment replaceFragment = new ReplaceFragment();
        mFragmentList.remove(0);
        mFragmentList.add(0,replaceFragment);

        mFgPagerAdapter.notifyDataSetChanged();
        Toast.makeText(this, "替换fragment成功", Toast.LENGTH_SHORT).show();
    }

我们将首位的Fragment1移除掉,然后再将ReplaceFragment 添加到首位,最后调用notifyDataSetChanged()方法来尝试更新视图,实际效果如下:
FragmentPagerAdapter调用notifyDataSetChanged无效及解决方案_第2张图片
我们通过log日志可以看到,在页面初始化完毕之后,我们点击按钮进行Fragment的数据刷新,发现并没有任何效果。

重写getItemPosition方法

这时在网上查找相关问题的解决方法,说我们可以重写FragmentPagerAdaptergetItemPosition方法,来使Fragment重新创建,好的,我们在MyFgPagerAdapter中重写该方法

    @Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;
    }

加上上面的代码之后,我们再运行项目看一下效果
FragmentPagerAdapter调用notifyDataSetChanged无效及解决方案_第3张图片通过log日志我们发现现在点击按钮时我们的Fragment确实销毁重建了,可是并没有达到我们将Fragment1替换成ReplaceFragment的目的。
接着查找资料,发现都提到了FragmentPagerAdapter中的instantiateItem这个方法,我们点进FragmentPagerAdapter源码简单的看一下该方法

    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // Do we already have this fragment?
        //获取fragment的名字
        String name = makeFragmentName(container.getId(), itemId);
        //从内存中根据fragment的名字取出fragment
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        //如果存在,直接attach
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            mCurTransaction.attach(fragment);
        } else {
            fragment = getItem(position);
            if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId));
        }
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }

我们在使用FragmentPagerAdapter时,每次实例化一个Fragment时都会调用instantiateItem这个方法,而这个方法里面原来是先通过FragmentManager从内存里面获取fragment,如果已经存在了,直接就attach了,不会再重新创建。而我们只是从List中简单的移除了Fragment1,并没有从内存中移除它,所以当我们调用notifyDataSetChanged方法时 ,ReplaceFragment并不会被创建。解决方法就是我们在调用notifyDataSetChanged方法之前,先将内存当中的fragment全部移除掉,我们在MyFgPagerAdapter中增加一个方法达到我们的目标, 代码如下:

    public void setFragments(List fragments) {
        if(this.fragmentList != null){
            FragmentTransaction ft = fm.beginTransaction();
            for(Fragment f:this.fragmentList){
                ft.remove(f);
            }
            ft.commit();
            ft=null;
            fm.executePendingTransactions();
        }
        this.fragmentList = fragments;
        notifyDataSetChanged();
    }

这里需要注意一点,因为我们在MyFgPagerAdapter的构造函数当中直接将fragmentList 指向了我们MainActivity中的mFragmentList,而我们又是在执行setFragments这个方法之前将mFragmentList中的数据改变了,这就会导致我们遍历fragmentList 从内存中移除fragment时遗漏掉Fragment1,所以对MyFgPagerAdapter的构造函数稍作修改,代码如下:

    public MyFgPagerAdapter(FragmentManager fm, List fragmentList, List titleList) {
        super(fm);
        this.fm = fm;
        //this.fragmentList = fragmentList;
        this.fragmentList = new ArrayList<>();
        this.fragmentList.addAll(fragmentList);
        this.titleList = titleList;
    }

然后对Fragment1中的按钮方法稍作修改:

    @Override
    public void replace() {
        //默认replace第一个
        ReplaceFragment replaceFragment = new ReplaceFragment();
        mFragmentList.remove(0);
        mFragmentList.add(0,replaceFragment);

        mFgPagerAdapter.setFragments(mFragmentList);

        Toast.makeText(this, "替换fragment成功", Toast.LENGTH_SHORT).show();
    }

接着再次运行项目,效果如下:

哈哈,总算达到我们想要的结果啦,咦?不过怎么感觉有点不对劲呢。Fragment1确确实实是成功的被替换了,可是标题2对应的Fragment2怎么是空白呢,查看log日志发现Fragment2在销毁之后重新创建时只执行到了onCreate方法,所以还没有创建视图,导致界面为空,而我也没有设置懒加载,为什么onCreateView没有执行呢,欢迎知道原因的小伙伴文章下方留言解答,谢谢!

不知道有没有细心的小伙伴发现,我们在使用FragmentPagerAdapternotifyDataSetChanged方法时,FragmentonSaveInstanceState方法并没有执行


FragmentStatePagerAdapter

在查找相关资料说可以将适配器更换成FragmentStatePagerAdapter也可以满足我们最初的需求。我们更换成FragmentStatePagerAdapter试一下,MainActivity中代码稍作修改 :

将适配器稍作更改

mFgStatePagerAdapter = new MyFgStatePagerAdapter(getSupportFragmentManager(),mFragmentList,mTitleList);
mViewPager.setAdapter(mFgStatePagerAdapter);

对应的按钮事件也稍作更改


    @Override
    public void replace() {
        //默认replace第一个
        ReplaceFragment replaceFragment = new ReplaceFragment();
        mFragmentList.remove(0);
        mFragmentList.add(0,replaceFragment);

        mFgStatePagerAdapter.notifyDataSetChanged();
        Toast.makeText(this, "替换fragment成功", Toast.LENGTH_SHORT).show();
    }

其中MyFgStatePagerAdapter代码如下:

public class MyFgStatePagerAdapter extends FragmentStatePagerAdapter {

    private FragmentManager fm;
    private List fragmentList;
    private List titleList;

    public MyFgStatePagerAdapter(FragmentManager fm, List fragmentList, List titleList) {
        super(fm);
        this.fragmentList = fragmentList;
        this.titleList = titleList;
    }



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

    @Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;
    }

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


    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        return titleList.get(position);
    }

我们在MyFgStatePagerAdapter中依旧重写了getItemPosition方法,返回POSITION_NONE。值得注意的是我们没有再提供一个方法来清除内存中的Fragment,这是因为FragmentStatePagerAdapterinstantiateItem方法和FragmentPagerAdapter稍有不同,我们查看一下FragmentStatePagerAdapterinstantiateItem方法

  @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
    //直接调用getItem方法获取Fragment
        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

可以看到在FragmentStatePagerAdapterinstantiateItem方法中,不会从内存中获取Fragment,而是直接调用我们重写的getItem方法,返回我们list中的Fragment,所以我们也就不需要重写instantiateItem方法了。
我们运行项目,看一下更换成FragmentStatePagerAdapter后的效果:
FragmentPagerAdapter调用notifyDataSetChanged无效及解决方案_第4张图片
nice!可以看到我们使用FragmentStatePagerAdapter之后,不但满足了我们最初的需求,而且还解决了我们使用FragmentPagerAdapter导致相邻的Fragment2视图不加载的问题!


初步结论

  • 不论我们使用的是FragmentPagerAdapter还是FragmentStatePagerAdapter,如果想通过AdapternotifyDataSetChanged方法来实现fragment视图的更新,我们都必须重写getItemPosition方法,并且返回POSITION_NONE
    @Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;
    }
  • 当使用FragmentPagerAdapter如果需要对某个Fragment进行替换时,我们还必须将内存中的Fragment清除,最后调用notifyDataSetChanged方法才能够替换成我们想要更换的Fragment。(不过会导致相邻的Fragment视图不加载,这个问题还未解决 ,欢迎知道原因的小伙伴下方留言,谢谢0.0)
  • 使用FragmentStatePagerAdapter时只需要参照第一条重写getItemPosition方法,不论是Fragment视图刷新还是替换,都可以正常调用notifyDataSetChanged方法来实现。

最后附上Demo源码:下载

参考文章

为什么调用 FragmentPagerAdapter.notifyDataSetChanged() 并不能更新其 Fragment

你可能感兴趣的:(Android)