Android Fragment+ViewPager 组合,一些你不可不知的注意事项

前面两篇文章中,对 Fragment 的基本使用、常见问题和状态恢复做了详细的分析总结。除了在 Activity 中单独使用 Fragment,Fragment + ViewPager 组合也是项目中使用非常频繁的方式,本文再来总结一下这种组合使用时的注意事项。在此之前,如果你对 Fragment 的认知和使用还有不清楚的地方,一定要先阅读前面两篇文章:

基本使用


对于这种组合使用,ViewPager 提供了两种页面适配器来管理不同 Fragment 之间的滑动切换:FragmentPagerAdapterFragmentStatePagerAdapter。先来看一下他们的基本使用,稍后再分析二者之间的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//    private class ContentPagerAdapter extends FragmentStatePagerAdapter{
private class ContentPagerAdapter extends FragmentPagerAdapter{

public ContentPagerAdapter(FragmentManager fm) {
super(fm);
}

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

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

如上述代码所示,没有特别要求的话,无论是哪种适配器类,实现起来都比简单,不需要像普通的 ViewPager + View 组合那样,还需处理视图的初始化工作(instantiateItem方法)和销毁(destroyItem方法)等。FragmentPagerAdapterFragmentStatePagerAdapter 在内部已经默认实现了这些功能。

两种 PagerAdapter 区别


源码定义中已经很清楚地描述了 FragmentPagerAdapterFragmentStatePagerAdapter 的区别:

FragmentPagerAdapter

Implementation of PagerAdapter that represents each page as a Fragment that is persistently kept in the fragment manager as long as the user can return to the page.

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider FragmentStatePagerAdapter.

FragmentStatePagerAdapter

Implementation of PagerAdapter that uses a Fragment to manage each page. This class also handles saving and restoring of fragment’s state.

This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.

总结归纳如下:

使用 FragmentPagerAdapter 时,ViewPager 中的所有 Fragment 实例常驻内存,当 Fragment 变得不可见时仅仅是视图结构的销毁,即调用了 onDestroyView 方法。由于 FragmentPagerAdapter 内存消耗较大,所以适合少量静态页面的场景。

使用 FragmentStatePagerAdapter 时,当 Fragment 变得不可见,不仅视图层次销毁,实例也被销毁,即调用了 onDestroyViewonDestroy 方法,仅仅保存 Fragment 状态。相比而言, FragmentStatePagerAdapter 内存占用较小,所以适合大量动态页面,比如我们常见的新闻列表类应用。

“Talk is cheap, show me the code.” 如果这样表达还是不能理解二者之间的区别的话,最好的办法就是用代码来表达。

新建一个名为 BaseFragment 的基类,继承自 Fragment,重写
setUserVisibleHint 和 各个生命周期函数,添加日志打印。然后新建四个子类,分别命名为 OneFragment、TwoFragment、ThreeFragment 和 FourFragment,按照基本使用方法写好代码,运行并滑动页面,查看日志打印。

由于代码较为简单,考虑内容长度,这里就不贴相关代码,主要描述思想。对应日志截图如下:

使用 FragmentPagerAdapter 时:

使用 FragmentStatePagerAdapter 时:

图中做了相应标记说明,二者区别一目了然,无需过多解释。出现这样的区别,其实从源码中的 instantiateItemdestroyItem 也能读出一二,感兴趣的话可以翻看一下。

Fragment 懒加载


懒加载,顾名思义,是希望在展示相应 Fragment 页面时再动态加载页面数据,数据通常来自于网络或本地数据库。这种做法的合理性在于用户可能不会滑到一下页面,同时还能帮助减轻当前页面数据请求的带宽压力,如果是用户使用流量的话,还能避免无用的流量消耗。

从上面的截图中可以看出,ViewPager 在展示当前页面时,会同时预加载下一页面。事实上,可以通过 ViewPager 提供的 setOffscreenPageLimit(int limit) 方法设置 ViewPager 预加载的页面数量,默认值为 1,并且这个参数的值不能小于 1。所以也就无法通过这个方法实现 ViewPager 中 Fragment 的懒加载,一定要改 ViewPager 的话只能通过自定义一个 Viewpager 类来实现,这种做法就比较繁琐。其实可以从 Fragment 下手。

ViewPager 本质上是通过 Fragment 调用 setUserVisibleHint 方法实现 Fragment 页面的展示与隐藏,这一点从FragmentPagerAdapterFragmentStatePagerAdapter 的源码和上面的截图中都可以看出。那么对应的解决方案就有了,自定义一个 LazyLoadFragment 基类,利用 setUserVisibleHint 和 生命周期方法,通过对 Fragment 状态判断,进行数据加载,并将数据加载的接口提供开放出去,供子类使用。参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public abstract class LazyLoadFragment extends BaseFragment {

protected boolean isViewInitiated;
protected boolean isDataLoaded;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInitiated = true;
prepareRequestData();
}

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

public abstract void requestData();

public boolean prepareRequestData() {
return prepareRequestData(false);
}

public boolean prepareRequestData(boolean forceUpdate) {
if (getUserVisibleHint() && isViewInitiated && (!isDataLoaded || forceUpdate)) {
requestData();
isDataLoaded = true;
return true;
}
return false;
}

}

然后在子类 Fragment 中实现 requestData 方法即可。这里添加了一个 isDataLoaded 变量,目的是避免重复加载数据。考虑到有时候需要刷新数据的问题,便提供了一个用于强制刷新的参数判断。这种思路来自于 这篇文章,在此基础上做了一些修改。实际上,在项目开发过程中,还需处理网络请求失败等特殊情况,我想,了解原理之后,这些问题都不再是问题。

Fragment 状态恢复问题


前文描述FragmentPagerAdapterFragmentStatePagerAdapter 的区别时有提到,这两种适配器类默认都会保存 Fragment 状态,包括 View 状态和成员变量数据状态。需要注意的是,View 状态包括的内容很多,比如用户在 EditText 中输入的内容、ScrollView 滑动的位置纪录等。

有关 Fragment 的具体使用细节和注意事项可以参考这篇文章:Android Activity 和 Fragment 状态保存与恢复的最佳实践。这里我说说另外两种简单粗暴的做法。

一种是通过 setOffscreenPageLimit 方法设置保留视图结构的 Fragment 数量,比较简单,比如保留所有 Fragment 视图结构: mContentVp.getAdapter().getCount()-1;另一种就是重写适配器的 Fragment 相关方法,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//    private class ContentPagerAdapter extends FragmentStatePagerAdapter {
private class ContentPagerAdapter extends FragmentPagerAdapter {

private FragmentManager fragmentManager;

public ContentPagerAdapter(FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}

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

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

@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
this.fragmentManager.beginTransaction().show(fragment).commit();
return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = fragmentList.get(position);
fragmentManager.beginTransaction().hide(fragment).commit();
}

}

这种处理下,也就不用区分使用的是哪种适配器类,通过重写 instantiateItemdestroyItem 方法,使用 show 和 hide 方法处理 Fragment 的展示与隐藏,这样,视图结构就不会销毁,换一种角度解决了 Fragment 状态保存与恢复的问题。

可以看出,使用这两种处理方式时,Fragment 实例均保存在内存中,具有一定内存消耗,适合于页面较少的情况。至于大量页面,还是推荐通过 Fragment 自带的状态保存与恢复方式处理。