什么是PagedList
PagedList是Google官方推出的一个分页加载库,即PagedList Library。这个库较完善的支持了“分页加载数据”这个常见的应用场景。PagedList一端可以无缝衔接RecyclerView,另一端可以从本地数据库或者网络获取数据源。
组成和基本原理
PagedList库主要由三部分组成:
- DataSource:管理数据源
- PagedList:封装后的分页数据
- PagedListAdapter:代替RecyclerAdapter的适配器
其中,DataSource用来管理数据源。数据可能是来自本地数据库,也可能直接来自网络,总之最后都是要交给DataSource来管理的。PagedList是具体的分页数据,在DataSouce产出一组数据后,会封装为PagedList对象。PagedListAdapter可以直接代替RecyclerAdapter,在有新的PagedList对象到泪后,交给PagedListAdapter来处理,在内部进行数据的对比分析后,再更新RecyclerView列表。
DataSource
DataSource是数据源管理类,完美需要实现他的一些接口,在这些接口回调时从网络或者本地数据库获取数据。DataSource有三个现成的实现类可以直接用:
- PositionalDataSource:适用于数据总量已知且不可变,我们从中直接查询指定位置的部分数据。(也不是说完全不可变,只是总量变化时会有很多问题需要额外处理)
- PageKeyedDataSource:以页为单位加载任意数量的数据,这里的key有页码的意思,即每次加载的都是指定页码的数据
- ItemKeyedDataSource:同样以页为单位加载任务数量的数据,但这里的key不是页码,而是每一页请求所需要的一个条件,每次加载的都是满足该条件的下一页数据
此外如果本地数据库用了Room,也可以自动生成一个DataSouce,不用我们手动创建。只需要在定义Room接口的时候返回一个 DataSource.Factory
,而Room的自动生成的DataSource是继承自PositionalDataSource的。
另外这里有一点,PositionalDataSource是跟另外两者完全不同的,另外两个是直接继承自ContigugousDataSource的,在源码中可以看到是“连续”的,这里的连续的意思是,每一页的数据一定依赖于上一页的数据,所以数据是有连续性的;而PositionalDataSource的数据表面上看也是一页跟一页的,实际上他每一页的数据只依赖于一个索引,这个索引和第一页、第二页不是一个概念,而是指在全部数据中的位置,类似于目录中的具体页码。
PagedList
PagedList是数据封装类,从DataSource,经过封装后成为PagedList对象。PagedList可以配置分页加载时每一页加载的数量,初始化时第一页加载的数量,加载下一页的阈值等。此外还可以添加一些回调,在数据更新、加载时会触发,比较重要的一个是 BoundaryCallback
,这个回调在一页数据加载完需要下一页时会触发。一个常见的场景是,数据来源虽然是网络,但是在本地会有一个缓存,网络数据会先在本地缓存,而后列表页从本地缓存中读取数据。这种情况下,BoundaryCallback
就变得尤为重要,我们可以在把 DataSource
设置为来自本地缓存,在每一页数据加载完触发回调时从网络获取下一页数据,成功获取到数据后把新的数据写入到本地缓存中。而本地缓存读取时用 LiveData
,这样只要缓存有更新,就会触发页面数据的更新。
PagedListAdapter
PagedListAdapter是用来代替RecylerAdapter的,创建时需要提供一个 DiffUtil.ItemCallback
对象,这是Adapter的核心。在有新数据添加到Adapter中时,内部会用这个对象来计算到底哪些item需要更新,之后再更新整个RecyclerView。
源码简析
一个简单的业务模型
这里以一个常用的业务模型流程为例来分析PagedList到底做了什么。这个业务模型很简单,就是首先数据来自于网络(Retrofit),从网络获取到数据后,直接写入数据库(Room),之后从数据库获取一个DataSource,来作为数据源,这样每当数据库有更新时,都会通过LiveData来告知PagedListAdapter去更新当前的PagedList数据。代码上大概就是:
首先是用Room来创建一个DataSource的获取:
1 |
|
再创建一个分页回调,这个回调在数据库为空或者读到头需要新数据时会回调:
1 | class FeedBoundaryCallback() : PagedList.BoundaryCallback<Feed>() { |
再创建 LiveData<PagedList>
:
1 | val list = LivePagedListBuilder<Int, Feed>(factory, 20).setBoundaryCallback(feedBoundaryCallback).build() |
最后监听上面的LiveData,在有新的数据到来时调用 submit
:
1 | list.observe(this, Observer { |
接下来就以这个模型来简单分析一下背后的实现原理。
DataSource
第一步先来看看DataSouce是如何创建的。因为使用了Room,Room会自动生成DataSource的代码,我们无需写具体实现
1 |
|
query
方法返回了一个DataSource.Factory,重写了 create()
方法并返回了一个 LimitOffsetDataSource
实例,看下这是什么东西:
1 | public abstract class LimitOffsetDataSource<T> extends PositionalDataSource<T> { |
LivePagedListBuilder
接下来看看 LiveData<PagedList>
是如何创建的:
1 | //在我们的代码中是调了build()方法来创建的 |
这里是创建了一个 ComputableLiveData
,这又是什么呢
ComputableLiveData
这个LiveData我们平常极少会用到,看下他的源码注释:
1 | /** |
翻译一下就是这是一个在被订阅时才会开始计算;另外也提供了主动计算的方法。他的构造函数:
1 | public ComputableLiveData(@NonNull Executor executor) { |
再看看 mRefreshRunnable
的定义:
1 |
|
此外还提供了主动申请计算的方法:
1 | // invalidation check always happens on the main thread |
最后,最终获取计算结果的方法:
1 |
|
简单总结一下,这个LiveData内部通过保存另一个实际的LiveData实例,在其被订阅时,或者是主动申请时,才会获取最新的计算结果,将其通过PostValue方法交给内部的LiveData。因为涉及到多线程,所以用了一些列AtomicBoolean变量来作为锁,保证线程安全。
PagedList的创建
前面铺垫了那么多,最后还是要创建PagedList,还是用了构建者模式:
1 | mList = new PagedList.Builder<>(mDataSource, config) |
来看看 build()
方法做了什么:
1 | public PagedList<Value> build() { |
这里会根据当前DataSouce的类型,来创建对应的不同的PagedList。
PagedListAdapter
最后再看看 PagedListAdapter
里面做了什么。
首先看创建这里,我们会传一个 DiffUtil.ItemCallback
的对象到构造器中:
1 | public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder> |
之后,我们会在监听到 LiveData
回调时,调用这个 Adapter
的submit()
方法把最新的 PagedList
传过去:
1 | public void submitList(@Nullable PagedList<T> pagedList) { |
一层一层调用,最后还是这个 submit()
的具体实现还是在 AsyncPagedListDiffer
内:
1 | public void submitList(@Nullable final PagedList<T> pagedList, |
看下差分是怎么做的:
1 | static <T> DiffUtil.DiffResult computeDiff( |
再看看差分的结果是怎么反应到UI上的:
1 | void latchPagedList( |
具体的执行:
1 | static <T> void dispatchDiff(ListUpdateCallback callback, |
可以看到整个流程中多次出现了 callback.onInserted()
或者 callback.onRemoved()
系列方法,他们的具体实现是:
1 | public final class AdapterListUpdateCallback implements ListUpdateCallback { |
这里就是我们很熟悉的 notifyXXX()
系列方法了,也就是说不论是全部加载新数据也好,全部删除旧数据也好,或者差分计算后应用新结果,最后都是通过自己调用不通过的 notifyXXX()
系列方法来实现UI更新的。
总结
那么结合前面LiveData实例的创建,不难看出整个流程:
- 创建一个
ComputableLiveData
对象,在订阅时进行计算,调用compute()
方法获取最新的数据并通知给外部调用者 - 在
compute()
方法内,创建一个LimitOffsetDataSouce
的实例和一个PagedList
的实例,让后者持有前者,并给DataSouce添加了一个刷新回调 - 在数据库刷新时,会通知
LimitOffsetDataSouce
的invalidate()
,此时会触发第二步中添加的刷新回调,在这个回调内又会去触发ComputableLiveData
的刷新,再进行一次计算 - 每次
ComputableLiveData
重新计算后都会根据当前DataSouce类型去创建一个PagedList
对象 - 在有新的数据到来后,会调
submit()
方法来更新PagedList
此外,还有一些从源码中得出的结论:
- 因为数据刷新的时候会重新创建
DataSouce
和PagedList
,所以从外部持有一个对他们的引用是没有意义的。如果需要保存一些生命周期更长久的变量,不要放在这里面。 - 在需要刷新全部数据时,可以直接清空数据库,这样会触发
LimitOffsetDataSource
中的刷新回调,继而通知ComputableLiveData
重新计算。 - 在调用
observe()
注册监听后,才会计算,才会创建第一个DataSoue
和PagedList
- 在需要给
Adapter
加Header
或者Footer
时,可以重写DataObserver
,通过修改具体的每个notifyXXX()
方法的索引参数来规避一些更新UI的问题