Android Fragment+ViewPager 组合使用

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

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

基本使用


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

//    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();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

两种 PagerAdapter 区别


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

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 变得不可见,不仅视图层次销毁,实例也被销毁,即调用了 onDestroyView 和 onDestroy 方法,仅仅保存 Fragment 状态。相比而言, FragmentStatePagerAdapter 内存占用较小,所以适合大量动态页面,比如我们常见的新闻列表类应用。

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

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

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

使用 FragmentPagerAdapter 时:

使用 FragmentStatePagerAdapter 时:

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

Fragment 懒加载


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

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

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

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;
    }

}
  • 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

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

Fragment 状态恢复问题


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

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

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

//    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();
        }

    }
  • 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

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

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

Fragment实现类似activity onResume()功能,控制fragment可见与不可见

2017年08月22日 14:18:41 growing_c 阅读数:3971

众所周知,fragment的onResume()和onPause()方法是和activity绑定在一起的,此时fragment的onResume方法并不能确定在fragment切换前后台时会调用,而app开发中经常会需要在前后台切换时做一些操作,在activity中一般直接在onResume()中操作,而fragment中似乎没有这么方便的原生方法。

和别人合作做某一项目的时候,发现他的fragment中是使用setUserVisibeHint来区分fragment的显示与否,经研究发现,当使用ViewPager+Fragment组合的时候会多次调用setUserVisibleHint方法,然而此方法却有很多缺点,摘一段源码中的note:

 

 
  1. * <strong>Note:</strong> This method may be called outside of the fragment lifecycle.

  2. * and thus has no ordering guarantees with regard to fragment lifecycle method calls.

既然有这些缺点,那么在这个方法中来进行数据加载和控件的操作 必然是不安全的

一.简单情景下的封装

于是我们就想要封装一个简单易用的方法来进行数据加载和控件操作

暂时先不管ViewPager+Fragment组合的情况,在一般使用Fragment的情景中 

通过 onResume(),onPause(),onHiddenChanged()这三个方法来实现了我们的需求。其中onResume()和onPause()方法是和绑定的activity的生命周期绑定的,另外onHiddenChanged()是在fragment是否可见状态改变的时候调用的,于是就有了以下的解决方案,不再赘述,直接上代码:

 
  1. public abstract class BaseFragment extends Fragment {

  2. @Override

  3. public void onCreate(Bundle savedInstanceState) {

  4. super.onCreate(savedInstanceState);

  5. }

  6. /**

  7. * fragment可见的时候操作,取代onResume,且在可见状态切换到可见的时候调用

  8. */

  9. protected void onVisible() {

  10. }

  11. /**

  12. * fragment不可见的时候操作,onPause的时候,以及不可见的时候调用

  13. */

  14. protected void onHidden() {

  15. }

  16. @Override

  17. public void onResume() {//和activity的onResume绑定,Fragment初始化的时候必调用,但切换fragment的hide和visible的时候可能不会调用!

  18. super.onResume();

  19. if (isAdded() && !isHidden()) {//用isVisible此时为false,因为mView.getWindowToken为null

  20. onVisible();

  21. }

  22. }

  23. @Override

  24. public void onPause() {

  25. if (isVisible())

  26. onHidden();

  27. super.onPause();

  28. }

  29. @Override

  30. public void onHiddenChanged(boolean hidden) {//默认fragment创建的时候是可见的,但是不会调用该方法!切换可见状态的时候会调用,但是调用onResume,onPause的时候却不会调用

  31. super.onHiddenChanged(hidden);

  32. if (!hidden) {

  33. onVisible();

  34. } else {

  35. onHidden();

  36. }

  37. }

代码中的注释已经很详尽了,在fragment可见的时候会调用onVisible()不可见的时候会调用onHidden(),继承这个baseFragment你就可以快乐的使用onVisible()和onHidden()来进行数据加载咯~~

注意:如果您的fragment是配合viewpager使用的,因为viewpager当前页的左右两个fragment都会默认加载,且切换的时候并不会调用onhiddenChanged()方法,所以我上面的方法在viewpager+fragment的组合下有一定的缺陷(例如:viewpager中有两个fragment,当从别的页面回到该viewpager所在页面时,两个fragment都会调用onvisible方法,而在左右滑动两个fragment的时候并不会调用onvisible和onhidden)

因此就有了下面的 加强版 适配ViewPager  setUserVisibleHint()的BaseFragment

二.包括ViewPager+Fragment的复杂情景通用封装

代码如下:

 
  1. public abstract class BaseFragment extends Fragment {

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

  3. /**

  4. * Fragment当前状态是否可见

  5. */

  6. protected boolean isVisible = false;

  7. /**

  8. * Fragment的view是否已创建

  9. */

  10. protected boolean mIsViewCreated = false;

  11. @Override

  12. public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

  13. View view = inflater.inflate(R.layout.xxx,container,false);

  14. System.out.println(TAG + "--onCreateView");

  15. mIsViewCreated = true;

  16. return view;

  17. }

  18. @Override

  19. public void onDestroyView() {

  20. super.onDestroyView();

  21. System.out.println(TAG + "--onDestroyView");

  22. mIsViewCreated = false;

  23. }

  24. @Override

  25. public void onDestroy() {

  26. super.onDestroy();

  27. System.out.println(TAG + "--onDestroy");

  28. }

  29. @Override

  30. public void setUserVisibleHint(boolean isVisibleToUser) {

  31. super.setUserVisibleHint(isVisibleToUser);

  32. if (!mIsViewCreated)//view没有创建的时候不进行操作

  33. return;

  34. if (getUserVisibleHint()) {

  35. if (!isVisible) {//确保在一个可见周期中只调用一次onVisible()

  36. isVisible = true;

  37. onVisible();

  38. }

  39. } else {

  40. if (isVisible) {

  41. isVisible = false;

  42. onHidden();

  43. }

  44. }

  45. }

  46. /**

  47. * 可见

  48. */

  49. protected void onVisible() {

  50. }

  51. /**

  52. * fragment不可见的时候操作,onPause的时候,以及不可见的时候调用

  53. */

  54. protected void onHidden() {

  55. }

  56. @Override

  57. public void onResume() {//和activity的onResume绑定,Fragment初始化的时候必调用,但切换fragment的hide和visible的时候可能不会调用!

  58. super.onResume();

  59. System.out.println(TAG + "--Base onResume");

  60. if (isAdded() && !isHidden()) {//用isVisible此时为false,因为mView.getWindowToken为null

  61. onVisible();

  62. isVisible = true;

  63. }

  64. }

  65. @Override

  66. public void onPause() {

  67. System.out.println(TAG + "--Base onPause");

  68. if (isVisible()||isVisible) {

  69. onHidden();

  70. isVisible = false;

  71. }

  72. super.onPause();

  73. }

  74. @Override

  75. public void onHiddenChanged(boolean hidden) {//默认fragment创建的时候是可见的,但是不会调用该方法!切换可见状态的时候会调用,但是调用onResume,onPause的时候却不会调用

  76. super.onHiddenChanged(hidden);

  77. System.out.println(TAG + "--Base onHiddenChanged:" + hidden);

  78. if (!hidden) {

  79. onVisible();

  80. isVisible = true;

  81. } else {

  82. onHidden();

  83. isVisible = false;

  84. }

  85. }

  86. }

此时 一样在继承的Fragment中 直接使用  onVisible()  以及 onHidden() 即可,此加强版适用于所有使用到fragment的地方,使用viewPager+fragment的亲们 可以愉快的开发啦~~

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

行知致简

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

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

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

打赏作者

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

抵扣说明:

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

余额充值