Android Paging 1 - chuwuwang/ReadingNote GitHub Wiki
Paging是Google 2018 IO大会最新发布的Jetpack中的一个组件,主要用于大数据的分页加载。
dependencies {
def paging_version = "2.1.2"
implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
// alternatively - without Android dependencies for testing
testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
// optional - RxJava support
implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}
以前,加载并展示大量数据就已成为各家应用中必不可少的业务场景,分页加载也就成了必不可少的方案。在现有的Android API中也已存在支持分页加载内容的方案, 比如:
-
CursorAdapter:它简化了数据库中数据到ListView中Item的映射,仅查询需要展示的数据,但是查询的过程是在UI线程中执行。
-
SupportV7包中的AsyncListUtil支持基于position的数据集分页加载到RecyclerView中,但不支持不基于position的数据集,而且它强制一个有限数据集中的null项必须展示Placeholder。
针对现有方案所存在的一些问题,Google推出了Android架构组件中的Paging Library。
Paging主要由三个部分组成:DataSource,PageList,PageListAdapter。
Paging主要是用来结合RecyclerView进行使用的。它的作用是能够逐渐地、优雅地加载所需要加载的数据。也就是一种分页方案。
Paging每次只会加载总数据的一小部分。因此它有下面的两个优点:
-
数据加载要求更小的带宽以及更少的系统资源。
-
在资源发生改变的情况下,app依然能够很快的做出响应。
-
LiveData< PagedList< DataBean > > 用LivePagedListBuilder生成。
-
LivePagedListBuilder 用 DataSource.Factory<Integer, DataBean> 和 PagedList.Config.Builder 生成。
-
DataSource.Factory<Integer, DataBean> 用 PositionalDataSource 生成。
-
PositionalDataSource是DataSource的子类,同时还有PageKeyedDataSource和ItemKeyedDataSource。
DataSource<Key, Value>从字面意思理解是一个数据源,其中key对应加载数据的条件信息,Value对应加载数据的实体类。
DataSource是一个抽象类,我们不能直接继承它实现它的子类。但是Paging库里提供了它的三个子类供我们继承用于不同场景的实现:
-
PageKeyedDataSource<Key, Value>:适用于目标数据根据页信息请求数据的场景,即Key字段是页相关的信息。比如请求的数据的参数中包含类似next/previous页数的信息。
-
ItemKeyedDataSource<Key, Value>:适用于目标数据的加载依赖特定item的信息,即Key字段包含的是Item中的信息。比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。
-
PositionalDataSource:适用于目标数据总数固定,通过特定的位置加载数据,这里Key是Integer类型的位置信息,T即Value。 比如从数据库中的1200条开始加在20条数据。
以上三种Datasource都是抽象类,使用时需实现请求数据的方法。三种Datasource都需要实现loadInitial()方法,各自都封装了请求初始化数据的参数类型LoadInitialParams。
不同的是分页加载数据的方法,PageKeyedDataSource和ItemKeyedDataSource比较相似,需要实现loadBefore()和loadAfter()方法,同样对请求参数做了封装,即LoadParams< Key >。PositionalDataSource需要实现loadRange(),参数的封装类为LoadRangeParams。
如果项目中使用Android架构组件中的Room,Room可以创建一个产出PositionalDataSource的DataSource.Factory:
@Query("select * from users WHERE age > : age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);
总的来说,DataSource就像是一个抽水泵,而不是真正的水源,它负责从数据源加载数据,可以看成是Paging与数据源之间的接口。
如果将DataSource比作抽水泵,那PagedList就像是一个蓄水池,但不仅仅如此。PageList是一个List的子类,支持所有List的操作,除此之外它主要有五个成员:
-
Executor mMainThreadExecutor:一个主线程的Excutor,用于将结果post到主线程。
-
Executor mBackgroundThreadExecutor:后台线程的Excutor。
-
BoundaryCallback< T > mBoundaryCallback:加载Datasource中的数据加载到边界时的回调。
-
Config mConfig:配置PagedList从Datasource加载数据的方式,其中包含以下属性:
-
pageSize:设置每页加载的数量。
-
prefetchDistance:预加载的数量,默认为pagesize。
-
initialLoadSizeHint:初始化数据时加载的数量,默认为pageSize*3。
-
enablePlaceholders:当item为null是否使用PlaceHolder展示。
-
-
PagedStorage< T > mStorage:用于存储加载到的数据,它是真正的蓄水池所在,它包含一个ArrayList< List< T > >对象mPages,按页存储数据。
PagedList会从DataSource中加载数据,更准确的说是通过DataSource加载数据,通过Config的配置,可以设置一次加载的数量以及预加载的数量。 除此之外,PagedList还可以向RecyclerView.Adapter发送更新的信号,驱动UI的刷新。
PagedListAdapte是RecyclerView.Adapter的实现,用于展示PagedList的数据。
它本身实现的更多是Adapter的功能,但是它有一个小伙伴PagedListAdapterHelper< T >, PagedListAdapterHelper会负责监听PagedList的更新,Item数量的统计等功能。这样当PagedList中新一页的数据加载完成时,PagedAdapte就会发出加载完成的信号,通知RecyclerView刷新,这样就省略了每次loading后手动调一次notifyDataChanged()。
除此之外,当数据源变动产生新的PagedList,PagedAdapter会在后台线程中比较前后两个PagedList的差异,然后调用notifyItem…()方法更新RecyclerView。
这一过程依赖它的另一个小伙伴ListAdapterConfig,ListAdapterConfig负责主线程和后台线程的调度以及DiffCallback的管理,DiffCallback的接口实现中定义比较的规则,比较的工作则是由PagedStorageDiffHelper来完成。
大致的4个不同如下:
-
Adapter不再继承自RecyclerView.Adapter,改为继承自PagedListAdapter,因为PagedListAdapter就是RecyclerView.Adapter的一个子类。
-
定义内部回调接口ItemCallbackImp继承自DiffUtil.ItemCallback< VideoInfo >,并且实例化一个父类引用指向子类ItemCallbackImp对象。
-
重写构造方法,无需参数传入,调用父类构造方法将mDiffCallback传入。
-
通onBindViewHolder中过调用getItem(position),获得指定位置的数据对象。因为Adapter中不再需要维护一个数据List了,PagedListAdapter中已经维护有,并且提供getItem()方法访问。
在使用Paging后,我们无需向Adapter中在传入数据源List,我们需要构造LiveData。
LiveData需要DataSource.Factory对象和PagedList.Config对象,只是实例化DataSource.Factory对象需要额外两个步骤。
DataSource.Factory是一个抽象类,实例化时需要实现create()函数,这个函数返回值是一个DataSource类对象。
DataSource是一个抽象类,他有三个实现子类。
所以我们捋一下思路。
-
首先要定义一个MyDataSource继承自DataSource的三个子类之一,
-
再定义一个MyDataSourceFactory继承自DataSource.Factory,返回值是MyDataSource。
-
然后实例化PagedList.Config,这个类提供有Builder(),比较简单。
-
最后将MyDataSourceFactory对象和PagedList.Config对象传入new LivePagedListBuilder()中得到LiveData数据源。将LiveData数据源和Adapter绑定是通过观察者模式实现,调用LiveData.observe()。
使用Paging加载数据主要有两种方式,一种是单一数据源的加载(本地数据或网络数据),另一种是多个数据源的加载(本地数据+网络数据)。
首先我们可以通过LivePagedListBuilder来创建LiveData为UI层提供数据。
如果数据源是DB,当数据发生变化,DB会推送(Push)一个新的PagedList(这里会依赖LiveData的机制)。如果是网络数据,即客户端无法知道数据源的变化,可以通过诸如滑动刷新的方式将调用DataSource的invalidate()方法来拉取(Pull)新的数据。
这种场景一般是先加载本地数据,加载完成后再加载网络数据,比较适合需要本地做缓存的业务。比如IM中的聊天消息,当打开聊天界面时先加载本地数据库中的聊天消息,加载完了再加载网络的离线消息。
这种场景需要为PagedList设置BoundaryCallback来监听加载完本地数据的事件,触发加载网络数据,然后入库,此时LiveData< PagedList >会推送一个新的PagedList, 并触发界面刷新。
具体使用案例可以参考Google Sample的PagingWithNetworkSample项目。
Paging作为Android架构组件库的一员,其特点主要还是在其架构思想上。Paging将分页的业务封装为一条完整的流水线,一个Pattern。其中各个组件之间存在联动的关系:
-
当PagedList创建时会立即从DataSource加载数据,触发loadInitial(), DataSource加载到数据后会更新PagedList,PagedList更新会通知到PagedAdapter并刷新UI。
-
UI上的展示会触发PagedAdapter的getItem(),随即触发PagedList的loadAround()方法从DataSource加载周围的数据...
整个过程Paging内部实现了线程的切换,数据的预加载,所有联动的关系都内聚到Paging中,这样使用时只需要关心加载数据的具体实现,并且在用户体验上,将会大大减少等待数据加载的时间和次数。