ViewPager和Fragment一篇就够了

ViewPager显示多Fragment使用问题

前言:每当使用ViewPager时,对于选用什么适配器,缓存多少页面,是否需要懒加载以及Fragment的数据刷新经常会有些疑问,网络上的答案很多,但是很少有一篇能够对一些疑问进行总结,本文主要在于记录,方便日后查看。

1.FragmentPagerAdapter和FragmentPagerStateAdapter的区别,使用场景

setOffScreenPageLimit(int limit)设置viewpager左右预加载页

区别:

FragmentPagerAdapter将每一个生成的Fragment保存在内存中,limit外Fragment没有销毁,生命周期为onPause->onStop->onDestroyView,onCreateView->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View,避免重复inflate。

FragmentPageAdater下Fragment的生命周期.png
private View mRootView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    Log.e(TAG, "onCreateView: page_" + mPosition);
    if (mRootView == null) {
        mRootView = inflater.inflate(R.layout.fragment_test, container, false);
        initView(mRootView);
    }
    return mRootView;
}

FragmentStatePagerAdapter对limit外的Fragment销毁,生命周期为onPause->onStop->onDestoryView->onDestory->onDetach, onAttach->onCreate->onCreateView->onStart->onResume。

FragmentStatePageAdapter下Fragment的生命周期.png

使用场景:对于需要缓存在内存中的固定较少数量的静态页面使用FragmentPagerAdapter,如引导页,Tab页面;对于拥有大量页面的情况应使用FragmentStatePagerAdapter避免占用大量内存,如图片预览

2.是否有必要在适配器的public Fragment getItem(int position)方法中返回缓存List<Fragment>中的Fragment

对于FragmentPagerAdapter,instantiateItem()先从FragmentManager.findFragmentByTag()中查找FragmentManager中List缓存的Fragment,取不到则会调用getItem(),所以对于缓存在内存中的FragmentPagerAdapter没有必要再使用一个List缓存Fragment,因为FragmentPagerAdapter会缓存每一个加载过的Fragment到内存中。

instantiateItem.png
makeFragmentName.png

对于FragmentStatePagerAdapter的instantiateItem()则会缓存limit左右的Fragment,超过limit则会回收,当Fragment没有缓存时重新调用getItem(),因为页面比较多,所以也没必要使用List缓存Fragment占用内存,否则FragmentStatePagerAdapter没有意义。

instantiateItem.png
fragments.png

3.ViewPager为什么要懒加载,什么情况适用?

ViewPager的setOffScreenPageLimit()方法默认limit为1,既会预加载左右页面,而为了节省流量,理想情况是当用户切换到该界面时才会调用网络请求获取数据。相关方法为setUserVisibleHint(),当前页面为true,预加载页面为false,只有Fragment从可见到不可见或者从不可见到可见时会调用,Fragment初次创建时setUserVisibleHint先于onCreateView()调用,所以可以由此判断Fragment是否初始创建。

ViewPager首次显示的页面经过方法调用setUserVisibleHint(false)->setUserVisibleHint(true)->onCreateView()...,所以该页面的数据加载放在onCreateView中;其它预加载页面预加载时setUserVisibleHint(false)->onCreateView()...,当选中该页面显示时调用setUserVisibleHint(true),所以预加载页面数据加载放在setUserVisibleHint中。

懒加载limit内Fragment的生命周期.png
/**
 * 延迟加载Fragment
 * Created by flying on 2017/3/2.
 */

public abstract class LazyLoadFragment extends BaseFragment {
    protected boolean bIsViewCreated;
    protected boolean bIsDataLoaded;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getLayoutResId(), container, false);
        initView(view);

        bIsViewCreated = true;

        if (getUserVisibleHint() && !bIsDataLoaded) {
            loadData();
            bIsDataLoaded = true;
        }
        return view;
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();

        bIsViewCreated = false;
        bIsDataLoaded = false;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);

        if (isVisibleToUser && bIsViewCreated && !bIsDataLoaded) {
            loadData();
            bIsDataLoaded = true;
        }
    }

    /**
     * @return 布局资源id
     */
    protected abstract int getLayoutResId();

    /**
     * 初始化View
     */
    protected abstract void initView(View view);

    /**
     * 加载数据
     */
    protected abstract void loadData();
}

因为懒加载需要设置setOffScreenPageLimit,所以适合有网络请求、页面较少且需要缓存的Tab页面,配合FragmentPagerAdapter使用,因为limit要包括所有的界面,在limit内FragmentStatePagerAdapter和FragmentPagerAdapter没有区别。

4.ViewPager刷新数据

一般使用PagerAdapter的notifyDataSetChanged方法来刷新数据,但是很多时候数据没有更新,先来看PagerAdapter的notifyDataSetChanged方法


notifyDataSetChanged.png

观察者模式:

观察者模式.png
nofifyChanged.png
onChanged.png

ViewPager中的PagerObserver实现了DateSetObserver


ViewPager_PagerObserver.png

ViewPager中的dataSetChanged方法会根据adapter.getItemPosition返回的值来判断是否DestroyItem


dataSetChanged.png

getItemPosition默认会返回POSITION_UNCHANGED,而ViewPager中dataSetChanged只有当返回POSITION_NONE时才会销毁页面重新创建
getItemPosition.png

继续看ViewPager中dataSetChanged方法
needPopulate.png

setCurrentItemInternal.png

接着到populate方法


populate.png

终于跑到adapter的instantiateItem方法了
addNewItem.png
FragmentPagerAdapter的inistantiateItem和destoryItem方法.png
FragmentStatePagerAdapter的destoryItem方法.png

所以如果想通过adapter.notifyDataSetChanged来刷新页面时,必须继承FragmentStatePagerAdapter,因为FragmentPagerAdapter会缓存Fragment,不会走getItem方法,同时将所要刷新页面的getItemPosition返回POSITION_NONE

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

还有其他的一种做法,拿到Fragment,通过Fragment中的public方法来刷新页面,由FragmentPagerAdapter的instantiateItem方法内部通过tag查找Fragment,因此可以保存其相同的tag

    private SparseArray<String> mTags = new SparseArray<>();
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        mTags.put(position, makeFragmentName(container.getId(), position));
        return super.instantiateItem(container, position);
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        mTags.remove(position);
        super.destroyItem(container, position, object);
    }

    private String makeFragmentName(int viewId, int position) {
        return "android:switcher:" + viewId + ":" + position;
    }

然后获取Fragment

Fragment fragment = getSupportFragmentManager().findFragmentByTag(mTags.get(position));
fragment.XXX();

由第二节FragmentStatePagerAdapter的instantiateItem方法可知,其保存时没有对Fragment添加tag,ViewPager中的Fragment也不能指定id,只有通过调用

  Fragment fragment = (Fragment)(fragmentStatePagerAdapter.instantiateItem(viewpager, position));

来获取Fragment


参考资料
1.ViewPager ,PagerAdapter,FragmentPagerAdapter,FragmentStatePagerAdapter
2.如何高效的使用ViewPager,以及FragmentPagerAdapter与FragmentStatePagerAdapter的区别
3.FragmentPagerAdapter与FragmentStatePagerAdapter区别
4.死磕Fragment生命周期
5.ViewPager刷新问题详解

总结

  1. 有Tab时:需要设置setOffScreenPageLimit,FragmentPageAdapter和FragmentStatePageAdapter效果相同,让Fragment都缓存在内存中,否则Fragment销毁了再次点击Tab选中又会重新创建会很突兀。需要网络请求时则执行延迟加载策略,无需网络请求时可以正常创建Fragment。

  2. 无Tab时:无需设置SetOffScreenPageLimit,因为默认limit是1,会预加载左右界面,不会显得突兀。页面较多时则选用占用内存少的FragmentStatePageAdapter,如浏览大图页面;页面较少时则选用加载到内存的FragmentPageAdapter, 如引导页,需要注意的是FragmentPageAdapter在limit外的Fragment没有销毁,生命周期为onPause->onStop->onDestroyView, onCreateView->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View。

  3. 如果需要刷新所有limit内的页面,继承FragmentStatePagerAdapter, 设置getItemPosition返回POSITION_NONE,再调用notifyDataSetChanged;如果只需要刷新单个页面,则通过获取Fragment的引用,再通过public方法来更新数据。

推荐阅读更多精彩内容