当前位置:   article > 正文

Google官方提供的分页加载解决方案

分页预加载方案

/   今日科技快讯   /

微信正式发布了《2019微信数据报告》。报告显示,2019年微信月活跃账户数达11.51亿,比去年同期增长6%。微信活跃高峰期为午饭前和下班后。微信表情排行榜中,捂脸、呲牙、偷笑、强、玫瑰位列前五位。微信运动日人均步数为6932步。微信搜一搜十大流行语包括我太难了、小姐姐、正能量、996、佛系、单身狗、雨女无瓜、柠檬精、硬核、妥妥的。此外,每月男性收到42%的红包,女性收到58%的红包。

/   作者简介   /

明天就是周六啦,提前祝大家周末愉快!

本篇文章来自蓅哖伊人为谁笑的投稿,详细分析了列表分页库Paging,相信对大家有所帮助。同时也感谢作者贡献的精彩文章!

蓅哖伊人为谁笑的博客地址:

https://www.jianshu.com/u/b742453da80a

/   前言   /

什么是Paging

Paging组件是Google新推出的分页组件,可以轻松帮助开发者实现RecyclerView中分页预加载以达到无限滑动的效果

如何引入Paging
  1. implementation 'android.arch.paging:runtime:1.0.0'

/   Paging分析   /

Paging工作原理图示

下图是官方提供的原理图

  • DataSource:数据源提供者,数据的改变会驱动列表的更新,因此,数据源是很重要的。

  • PageList:核心类,它从数据源取出数据,同时,它负责页面初始化数据+分页数据什么时候加载,以何种方式加载。

  • PagedListAdapter:列表适配器,通过DiffUtil差分异定向更新列表数据。

一共3种DataSource可选,取决于你的数据是以何种方式分页加载
ItemKeyedDataSource:基于cursor实现,数据容量可动态自增
PageKeyedDataSource:基于页码实现,数据容量可动态自增
PositionalDataSource:数据容量固定,基于index加载特定范围的数据

Paging工作原理代码分析

1. Paging的设计和传统的分页加载不大一样,它巧妙的融合LiveData的能力
首先看下如何使用Paging触发页面初始化数据加载的。

  1. //1.构建PagedList.Config对象,用以声明以何种方式分页
  2. PagedList.Config config = new PagedList.Config.Builder()
  3.                 .setPageSize(10)指定每次分页加载的条目数量
  4.                 .setInitialLoadSizeHint(12)指定初始化数据加载的条目数量
  5.                 //.setPrefetchDistance(10)指定提前分页预加载的条目数量,默认和pageSize相等
  6.                 //setMaxSize(100) 指定数据源最大可加载的条目数量
  7.                 //setEnablePlaceholders(false)指定未加载出来的条目是否用占位符替代,必须和setMaxSize搭配使用才有效
  8.                 .build();//最后构造出一个PagedList.Config对象,用以指定PagedList以何种方式分页
  9. //2.创建数据源工厂类,用来创建数据提供者
  10. DataSource.Factory factory = new DataSource.Factory() {
  11.         @NonNull
  12.         @Override
  13.         public DataSource create() {
  14.            return new ItemKeyedDataSource();
  15.         }
  16.     };
  17. //3.构建一个能够触发加载页面初始化数据的LiveData对象,并且把上面创建的DataSource.Factory和PagedList.Config 传递进去
  18. LiveData<PagedList<T>> pageData = new LivePagedListBuilder(factory, config)
  19.                 .setInitialLoadKey(0)//设置初始化数据加载的key(任意类型)
  20.                 //.setFetchExecutor()//指定使用哪个线程池进行异步工作
  21.                 .setBoundaryCallback(callback)//指定pagedList的第一条、最后一条,被加载到列表之上的边界回调callback。
  22.                 .build();//最后通过build方法 构建出LiveData对象。请注意它的 泛型是<PagedList<T>>
  23. //4.最后我们只需要拿到前面构建出来的LiveData对象注册一个Observer观察者,仅仅如此就可以触发页面初始化数据的加载了
  24. mViewModel.getPageData().observe(this, pagedList -> submitList(pagedList));
  25. 那么问题来了,LiveData和Paging数据加载是怎么配合的呢?怎么就产生火花了呢? 

2. 那先来看一下Paging是如何利用LiveData能力的,首先要看下ComputableLiveData这个类,它并不是LiveData的子类,但它利用了LiveData的onActive方法被激活的时机。

这也就解释了为么这段代码可以触发Paging的初始化数据的加载逻辑
mViewModel.getPageData().observe(this, pagedList -> submitList(pagedList));

  1. public abstract class ComputableLiveData<T{
  2. public ComputableLiveData(@NonNull Executor executor) {
  3.         mExecutor = executor;
  4.        //在构造函数中创建了一个LiveData对象。并且复写了它的onActive方法。至关重要,看过我LiveData源码分析的同学应该知道,
  5.        //该方法当且仅当有第一个Observer被注册到LiveData的时候,会被调用。
  6.        //而当onActive被调用的时候,它使用线程池执行了RefreshRunnable,实际上是触发了下面的compute方法
  7.         mLiveData = new LiveData<T>() {
  8.             @Override
  9.             protected void onActive() {
  10.                 mExecutor.execute(mRefreshRunnable);
  11.             }
  12.         };
  13.     }
  14. //一旦调用了该方法也便会触发下面的compute方法
  15.  public void invalidate() {
  16.    ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable)
  17.   }
  18. //虚方法,在LivePagedListBuilder中有唯一实现
  19.  protected abstract T compute();
  20. //获取在构造函数中创建的LiveData对象
  21.  public LiveData<T> getLiveData() { return mLiveData;}
  22. }

3.再来看一下上面的LivePagedListBuilder#build()方法又是是如何使用ComputableLiveData类的


噢,原来build()方法直接调用了内部方法create()并返回了LiveData对象

  1.     private static <Key, Value> LiveData<PagedList<Value>> create(
  2.             @Nullable final Key initialLoadKey,
  3.             @NonNull final PagedList.Config config,
  4.             @Nullable final PagedList.BoundaryCallback boundaryCallback,
  5.             @NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
  6.             @NonNull final Executor notifyExecutor,
  7.             @NonNull final Executor fetchExecutor) {
  8.        //该方法直接new 了一个 ComputableLiveData,并复写了它的`compute`方法,至关重要。
  9.         return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
  10.             private final DataSource.InvalidatedCallback mCallback =
  11.                     new DataSource.InvalidatedCallback() {
  12.                         @Override
  13.                         public void onInvalidated() {
  14.                            // 一旦监听到DataSource被置为无效,则调用ComputableLiveData#invalidate方法。
  15.                           //也就是会触发下面的compute方法
  16.                             invalidate();
  17.                         }
  18.                     };
  19.             @Override
  20.             protected PagedList<Value> compute() {
  21.                     //通过上面配置的dataSourceFactory创建一个数据源提供者
  22.                     mDataSource = dataSourceFactory.create();
  23.                    //并且于此同时会给mDataSource注册一个回调,用以监听该DataSource被置为无效的事件,
  24.                    //一旦DataSource被置为无效,则不能在提供数据,但会再次触发该方法(compute)再次创建一个新的mDataSource,重新执行下面的逻辑
  25.                     mDataSource.addInvalidatedCallback(mCallback);
  26.                    //compute方法是ComputableLiveData 的方法
  27.                   //会通过PagedList.Builder的build方法构造出一个PagedList,在跟下去就知道怎么触发初始化数据的加载了,来继续看下面
  28.                     PagedList  mList = new PagedList.Builder<>(mDataSource, config)
  29.                             .setNotifyExecutor(notifyExecutor)
  30.                             .setFetchExecutor(fetchExecutor)
  31.                             .setBoundaryCallback(boundaryCallback)
  32.                             .setInitialKey(initializeKey)
  33.                             .build();
  34.                 return mList;
  35.             }
  36.         }.getLiveData();
  37.     }

4. 触发初始化数据的加载

  1. ContiguousPagedList(
  2.             @NonNull ContiguousDataSource<K, V> dataSource,
  3.             @NonNull Executor mainThreadExecutor,
  4.             @NonNull Executor backgroundThreadExecutor,
  5.             @Nullable BoundaryCallback<V> boundaryCallback,
  6.             @NonNull Config config,
  7.             final @Nullable K key,
  8.             int lastLoad) {
  9.         super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
  10.                 boundaryCallback, config);
  11.         mDataSource = dataSource;
  12.         mLastLoad = lastLoad;
  13. // 如果当前datasource已经被置为无效了,则不会触发初始化数据加载的逻辑
  14.         if (mDataSource.isInvalid()) {
  15.             detach();
  16.         } else {
  17.            //上面的build方法调用了之后,经过一堆判断后便会走到这里,mDataSource.dispatchLoadInitial遍触发了我们上面配置的DataSource的oadInitial方法,也就是触发了页面初始化数据的加载了。
  18.            //其中mReceiver参数请注意,这是接收网络数据加载成功之后的callback
  19.            // PageResult.Receiver是内部类,它会判断本次分页回来的数据是初始化数据,还是分页数据,用以确定分页的状态
  20.           //而数据最终都是会被存储到PagedStorage<T>这个类中。它实际上是一个按页存储数据的ArrayList
  21.             mDataSource.dispatchLoadInitial(key,
  22.                     mConfig.initialLoadSizeHint,
  23.                     mConfig.pageSize,
  24.                     mConfig.enablePlaceholders,
  25.                     mMainThreadExecutor,
  26.                     mReceiver);

5. 触发分页数据的加载


分页数据加载的是在PagedListAdapter#getItem()方法中触发的,接着又会调用到ContiguousPagedList#loadAroundInternal方法。


该方法中会计算列表当前滑动状态下,列表后面还需要追加几条Item,列表前面还需要向前追加几条Item(Paging能向前向后分页加载数据,强大吧)。

  1. public class ContiguousPagedList{
  2. protected void loadAroundInternal(int index) {
  3.         int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
  4.                 mStorage.getLeadingNullCount());
  5.         int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
  6.                 mStorage.getLeadingNullCount() + mStorage.getStorageCount());
  7.         mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
  8.         if (mPrependItemsRequested > 0) {
  9.            //计算之后,如果需要向前追加的Item数量大于0,  schedulePrepend则会触发DataSource的LoadBefore方法
  10.             schedulePrepend();
  11.         }
  12.         mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
  13.         if (mAppendItemsRequested > 0) {
  14.             //计算之后,如果需要向后追加的Item数量大于0,  scheduleAppend则会触发DataSource的LoadAfter方法
  15.             scheduleAppend();
  16.         }
  17.     }
  18. }

经过上面的分析,相信如何使用Paging加载分页数据,Paging分页数据加载的工作原理都已经很清楚了。

列表数据差分异增量更新

如今,给列表RecyclerView设置Adapter,需要使用PagedListAdapter,并且要求传递一个DiffUtil.ItemCallback用以做列表新旧数据的差分异计算。如此便能使用Paging提供的列表数据差量更新能力了。

  1. class UserAdapter extends PagedListAdapter<User,ViewHolder{
  2.     public void UserAdapter(){
  3.            supernew DiffUtil.ItemCallback<User>() {
  4.            @Override
  5.           public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
  6.                return oldUser.getId() == newUser.getId();
  7.           }
  8.           @Override
  9.            public boolean areContentsTheSame( @NonNull User oldUser, {@literal @}NonNull User newUser) {
  10.                return oldUser.equals(newUser);
  11.            }
  12.        })
  13.      }
  14. }
UML图解Paging工作流程

Paging的设计、设计到的类十分的复杂,一篇文章难以概括其全部。下面用一张图描述Paging的工作原理

Paging美中不足

虽然Paging的设计十分优秀,能力什么强悍,但现阶段还有些美中不足,没有合适的解决方案


1. PagedList并不支持列表数据的增删改
2. Paging一旦有一次分页失败,便再也不会继续分页了
3. Paging如何先展示缓存数据再展示网络数据
4. Paging如果先添加了HeaderView,再展示加载的网络数据,列表定位会有问题


包括但不限于以上4个问题,目前网络上都没有合适的解决方案。本人基于对Jetpack核心组件的长期研究,已经找到了合适的解决方案,可以预测,即便后续的版本中谷歌提供了相关的Api解决上面的问题,我相信,跟本人的思路也会八九不离十。由于篇幅问题,解决方案具体的实现会在我的简书一一更新跟大家分享(绝对是网络上搜索不到的解决方案)。

项目地址:

https://github.com/mrme2014/jetpack

推荐阅读:

超长文,带你全面了解Kotlin的协程

这份AS快捷键大全,让你的开发效率快10倍

自定义View跟绘制流程懂吗?帮你搞定面试官

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号