ViewPager显示多Fragment使用问题
前言:每当使用ViewPager时,对于选用什么适配器,缓存多少页面,是否需要懒加载以及Fragment的数据刷新经常会有些疑问,网络上的答案很多,但是很少有一篇能够对一些疑问进行总结,本文主要在于记录,方便日后查看。
1.FragmentPagerAdapter和FragmentPagerStateAdapter的区别,使用场景
setOffScreenPageLimit(int limit)设置viewpager左右预加载页
区别:
FragmentPagerAdapter将每一个生成的Fragment保存在内存中,limit外Fragment没有销毁,生命周期为onPause->onStop->onDestroyView,onCreateView->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View,避免重复inflate。
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。
使用场景:对于需要缓存在内存中的固定较少数量的静态页面使用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到内存中。
对于FragmentStatePagerAdapter的instantiateItem()则会缓存limit左右的Fragment,超过limit则会回收,当Fragment没有缓存时重新调用getItem(),因为页面比较多,所以也没必要使用List缓存Fragment占用内存,否则FragmentStatePagerAdapter没有意义。
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中。
/**
* 延迟加载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方法
观察者模式:
ViewPager中的PagerObserver实现了DateSetObserver
ViewPager中的dataSetChanged方法会根据adapter.getItemPosition返回的值来判断是否DestroyItem
getItemPosition默认会返回POSITION_UNCHANGED,而ViewPager中dataSetChanged只有当返回POSITION_NONE时才会销毁页面重新创建
继续看ViewPager中dataSetChanged方法
接着到populate方法
终于跑到adapter的instantiateItem方法了
所以如果想通过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刷新问题详解
总结
有Tab时:需要设置setOffScreenPageLimit,FragmentPageAdapter和FragmentStatePageAdapter效果相同,让Fragment都缓存在内存中,否则Fragment销毁了再次点击Tab选中又会重新创建会很突兀。需要网络请求时则执行延迟加载策略,无需网络请求时可以正常创建Fragment。
无Tab时:无需设置SetOffScreenPageLimit,因为默认limit是1,会预加载左右界面,不会显得突兀。页面较多时则选用占用内存少的FragmentStatePageAdapter,如浏览大图页面;页面较少时则选用加载到内存的FragmentPageAdapter, 如引导页,需要注意的是FragmentPageAdapter在limit外的Fragment没有销毁,生命周期为onPause->onStop->onDestroyView, onCreateView->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View。
如果需要刷新所有limit内的页面,继承FragmentStatePagerAdapter, 设置getItemPosition返回POSITION_NONE,再调用notifyDataSetChanged;如果只需要刷新单个页面,则通过获取Fragment的引用,再通过public方法来更新数据。