ViewPager中Fragment的懒加载和可见状态监听

4,760 阅读6分钟

一. 前言

在Android开发中经常会使用到ViewPager, ViewPager如果和Fragment一起使用的话, 就要考虑懒加载和预加载的问题. ViewPager有个方法setOffscreenPageLimit 这个方法可以配置缓存数量. 那是不是直接设置0就可以实现懒加载了呢? 不是的, 查看源码:

  public void setOffscreenPageLimit(int limit) {
        if (limit < DEFAULT_OFFSCREEN_PAGES) {  //DEFAULT_OFFSCREEN_PAGES为1
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                    + DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES;
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }

设置0后会无效的, limit 还是被设置成默认的1

懒加载就是当用户滑动到当前的frament才能去加载数据, 这样避免加载了数据但是没有使用到, 造成了浪费.
预加载是为了提前加载数据, 让用户减少等待时间. 懒加载和预加载应该根据具体的业务要求去使用. 没有谁好谁坏之分. 但两者的前提都是要搞清楚Fragment在ViewPager中的生命周期, 下面先来弄清楚生命周期的调用.

二. Fragment在ViewPager中的生命周期

首先写一个简单的Activity里面有ViewPager 代码如下:

public class MainActivity extends AppCompatActivity {

    private ViewPager viewById;
    private List<Fragment> list;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewById = findViewById(R.id.vp);
        list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            SimpleFragment simpleFragment = SimpleFragment.newInstance(i);
            list.add(simpleFragment);
        }
        MineAdapter mineAdapter = new MineAdapter(getSupportFragmentManager(),list);
        viewById.setOffscreenPageLimit(1);
        viewById.setAdapter(mineAdapter);
    }
}

关于这个SimpleFragment代码就不用贴出来了 很简单就是在各个生命周期中加入log. 这个MineAdapter 很简单的. 如下:

public class MineAdapter extends FragmentPagerAdapter {
    List<Fragment>  mPages;
    public MineAdapter(FragmentManager fm, List<Fragment> pages) {
        super(fm);
        mPages=pages;
    }
    @Override
    public Fragment getItem(int i) {
        return mPages.get(i);
    }
    @Override
    public int getCount() {
        return mPages.size();
    }
}

运行程序 来看看Fragment生命周期 日志如下: 为了方便叙述第0个fragement简称为0号. 这时候其实是缓存数量为1

image.png
通过日志可以看到首次进入这个activity页面的时候. 首先加载的是0号和1号, 而且不是说等0号(当前显示界面)加载完 再加载1号. 而是0号和1号生命周期交错进行. 并且都一直走到了onResume方法. 滑动到1号 这时候 1号为当前展示界面. 0号和2号为缓存. 继续看日志
image.png
这时候1号只走了一个方法setUserVisibleHint true . 然后2号生命周期从onAttach到onResume. 再滑动一下, 这时候2号为当前展示界面. 这时候会缓存1号和3号, 0号进入销毁.
image.png

三. 总结生命周期中的规律

通过以上的日志 可以总结关键几点.

  1. setUserVisibleHint 方法都是比较先走的. 首次进入的时候同一个Fragment的setUserVisibleHint 要走两次 一次true, 一次false.
  2. 被预加载的Fragment的生命周期 除了setUserVisibleHint true没走之外 其他的生命周期也走了.
  3. 预加载的Fragment 到显示的时候 其实只走了 setUserVisibleHint true.
  4. Fragment 销毁的时候 只走到了onDestroyView方法 并没有走onDestroy onDetach方法. 这点对于 执行一些回收操作非常有必要了解.

四. 懒加载的实现

懒加载是滑动到当前Fragment的时候才去调用的方法. 一般在实际业务中就是滑到了要展示的页面去调接口获取数据.
写一个基础的BaseLazyFragment , 继承这个BaseLazyFragment 重写lazyInit()方法. 这个方法里写你需要执行的懒加载操作.

public class BaseLazyFragment extends Fragment {
    private boolean isViewPrepared; // 标识fragment视图已经初始化完毕
    private boolean hasFetchData; // 标识已经触发过懒加载数据

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        lazyFetchDataIfPrepared(); //经过了预加载页面, 然后展示 
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        isViewPrepared = true;
        lazyFetchDataIfPrepared(); //首次进入, 没有预加载直接加载数据
    }

    /**
     * 懒加载方法,获取数据什么的放到这边来使用,在切换到这个界面时才进行网络请求
     */
    private void lazyFetchDataIfPrepared() {
        // 用户可见fragment && 没有加载过数据 && 视图已经准备完毕
        if (getUserVisibleHint() && !hasFetchData && isViewPrepared) {
            hasFetchData = true; //已加载过数据
            lazyInit();
        }
    }

    /**
     * 执行需要懒加载的方法
     */
    protected void lazyInit() {
        Log.i("zmin........." + getArguments().getInt("key"), ".............加载完成数据");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        hasFetchData = false;
        isViewPrepared = false;
        Log.i("zmin........." + getArguments().getInt("key"), ".............onDestroyView");
    }
}

五. 预加载

1. 预加载在viewpager中

viewpager其实对预加载有很好的支持. 可以直接调用方法setOffscreenPageLimit来设置缓存的数量.

2. 监听可见和不可见的状态

在实际业务中, 可能存在这样一种需求. 虽然是需要预加载的, 但是要监听Fragment的可见状态. 比如Fragment中有视频播放. 如果Fragment可见的话就要播放. 不可见的时候就需要暂停. 这时候还需要考虑的是Fragment可能会跳转到其他界面. Fragment虽然可见和不可见有个生命周期方法setUserVisibleHint回调, 但是无法直接得知当前状态是一直不可见的,还是由可见转为不可见的 . 下面来实现这个功能 :

public class BaseAppearFragment extends Fragment {

    private boolean isViewPrepared; // 标识fragment视图已经初始化完毕
    private boolean hasAppear; //标识界面当前可见

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //当前fragment转为可见状态
        if (isVisibleToUser && isViewPrepared && !hasAppear) {
            onFragmentAppear();
            hasAppear = true;
        }
        //当前fragment转为不可见状态
        if (!isVisibleToUser && hasAppear) {
            onFragmentDismiss();
            hasAppear = false;
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.i("zmin........." + getArguments().getInt("key"), ".............onCreateView");
        return inflater.inflate(R.layout.activity_fragment, null);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        isViewPrepared = true;
        TextView tv = getView().findViewById(R.id.tv);
        tv.setText(String.valueOf(getArguments().getInt("key")));
        Log.i("zmin........." + getArguments().getInt("key"), ".............onViewCreated");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.i("zmin........." + getArguments().getInt("key"), ".............onResume(");
        if (getUserVisibleHint()) {
            onFragmentAppear();
            hasAppear = true;
        }
    }

    @Override
    public void onDestroyView() {
        isViewPrepared = true;
        super.onDestroyView();
        Log.i("zmin........." + getArguments().getInt("key"), ".............onDestroyView");
    }

    /**
     * 界面可见
     */
    public void onFragmentAppear() {
        Log.i("zmin........." + getArguments().getInt("key"), "......界面可见..onFragmentAppear");
    }

    /**
     * 界面由可见转为不可见
     */
    public void onFragmentDismiss() {
        Log.i("zmin........." + getArguments().getInt("key"), "...由可见转为不可见.........onFragmentDismiss");
    }
}

可以看到主要在在setUserVisibleHint和onResume方法中做判断. 因为Fragment切换的时候, 很多生命周期方法是不走的.

  1. 当0号滑动到1号的时候, 这时候1号只走setUserVisibleHint方法. onResume方法是不走的.
  2. 当1号跳转到其他界面再返回的时候 会执行onResume但是不执行setUserVisibleHint .
  3. 而首次进入的时候setUserVisibleHint 和onResume都执行.

六 总结

通过详细的日志 分析了Fragment生命周期的执行. 从而实现懒加载和预加载中对可见状态监听. 很多业务场景下需要用到. 如果要懒加载可以直接继承BaseLazyFragment 类即可. 如果要监听可见隐藏状态则可以继承 BaseAppearFragment . 如果还想自己去看看打印的日志. 可以clone代码, github地址 github.com/zmin666/Zmi… 希望这些总结对你有帮助.