Paging Library 是 Google 提出的分页加载库,本文将从以下几个方面对 Paging 进行介绍:
我们经常需要处理大量数据,但大多数情况下,只需要加载和显示其中的一小部分。如果去请求用户不需要的数据,势必会浪费用户设备的电量和带宽。如果数据比较多情况下,消耗用户的流量也会比较多。
Paging Library 是 Google 提出的分页加载库,它可以妥善的逐步加载数据, 解决上面提到的痛点。此外:
Paging Library 有这么多的特点,正是我们选择的使用它的主要原因。接下来分析一下它的组成及原理。
Paging Library 的原理是,将数据分解成多个 List,使用 RecyclerView 中的 Adapter来观察 LiveDdata 中的数据变化,在此基础上加上分页功能,从而实现逐步加载内容。
我们来看一下具体的实现过程:
看过上面Paging Library 的实现过程,我们来总结一下:
看过了 Paging Library 具体的执行过程,我们来分析一下它的组成。我们先来看一下 Paging Library 相关的类图。
Paging Library 的核心组件是 PagedList 和 DataSource,在上面的类图中,用不同的颜色进行了区分。下面我们分别来介绍。
PagedList 是一个集合类,它以分块的形式异步加载数据,每一块就称为一页。
在上面的类图中,我们可以看到:
在 PagedList 中,除了上面提到的这四个内部类的成员变量之外,还有两个比较重要的成员变量:
Paging Library 还提供了 LivePagedListBuilder类,用于获取 PagedList 中的 LiveData 对象,创建 LivePagedListBuilder 的参数,创建 DataSource.Factory 对象和分页配置对象。LivePagedListBuilder 获取 LiveData 对象的过程如下:
@AnyThread
@NonNull
@SuppressLint("RestrictedApi")
private static <Key, Value> LiveData<PagedList<Value>> create(
@Nullable final Key initialLoadKey,
@NonNull final PagedList.Config config,
@Nullable final PagedList.BoundaryCallback boundaryCallback,
@NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull final Executor notifyExecutor,
@NonNull final Executor fetchExecutor) {
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
@Nullable
private PagedList<Value> mList;
@Nullable
private DataSource<Key, Value> mDataSource;
private final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};
@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList<Value> compute() {
@Nullable Key initializeKey = initialLoadKey;
if (mList != null) {
initializeKey = (Key) mList.getLastKey();
}
do {
if (mDataSource != null) {
mDataSource.removeInvalidatedCallback(mCallback);
}
mDataSource = dataSourceFactory.create();
mDataSource.addInvalidatedCallback(mCallback);
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());
return mList;
}
}.getLiveData();
}
可以看到,创建 PagedList 对象,还是通过 PagedList 的内部类 Builder 的 build()方法。
如果倾向于使用 RxJava,而不是 LiveData,可以使用 RxPagedListBuilder, 它的构建方式与LivePagedListBuilder类似,不同之处在于RxPagedListBuilder返回一个 Observable 对象或 Flowable 对象,而不是 LiveData 对象。
再来看看 Paging Library 的另一个核心组成部分 DataSource。DataSource 是将数据加载到 PagedList 中的基类,任何数据都可以作为 DataSource 的来源,比如网络、数据库、文件等等。 DataSource.Factory 类可以用来创建 DataSource。
从上面的类图中,我们总结一下:
public abstract class DataSource {
/**
* Returns true if the data source guaranteed to produce a contiguous set of items,
* never producing gaps.
*/
abstract boolean isContiguous();
}
abstract class ContiguousDataSource extends DataSource {
@Override
boolean isContiguous() {
return true;
}
}
public abstract class PositionalDataSource extends DataSource {
@Override
boolean isContiguous() {
return false;
}
}
关于数据源产生的数据项是否为连续的,结合后面三种 DataSource 的使用场景更好理解。
我们来看一下 PositionalDataSource、PageKeyedDataSource 和 ItemKeyedDataSource 分别适用哪些场景:
假设数据源是数据库,Room 存储库可以作为 Paging Library 的数据源,对于给定查询的关键字,Room 可以从 DAO 中返回 DataSource.Factory 对象,从而无缝处理 DataSource 的实现。
假设数据库是从网络加载的数据缓存,从 DAO中返回 DataSource.Factory 对象,还需要另外一个分页组件,BoundaryCallback,当界面显示缓存中靠近结尾的数据时,BoundaryCallback 将加载更多的数据,在获得更多的数据后,Paging Library 将自动更新界面,不要忘记将创建的 BoundaryCallback 对象与之前创建的 LivePagedListBuilder 对象进行关联,关联之后,PagedList 就可以使用它了。
仅将网络作为数据源,在这种情景中,需要创建 DataSource 和 DataSource.Factory 对象,选择 DataSource 类型时, 需要综合考虑后端 API 的架构,如果通过键值请求后端数据,使用 ItemKeyedDataSource。
举个例子,我们需要在某个特定日期起,github的前 100 项提交,该日期将成为 DataSource 的键,ItemKeyedDataSource 允许自定义如何加载初始页,以及如何加载某个键值前后的数据,如果后端数据返回的是分页后的,那么我们可以使用 PageKeyedDataSource,比如 Github API 中的 SearchRepository 就可以返回分页数据,我们在 Github API 的请求中,指定查询的关键字和要查询哪一页,同时也可以指定每个页面的项数,不管网络数据源的创建方式是什么,都需要创建 DataSource.Factory对象,有了 DataSource.Factory 对象就可以创建 DataSource。
Paging Library 提供了 PagedListAdapter,可以将 PagedList 中的数据加载到 RecyclerView 中,PagedListAdapter 会在页加载时收到通知,收到新数据时,会使用 DiffUtil 精细计算更新。
在 PagedListAdapter 中使用的是 AsyncPagedListDiffer,从名字就能看出这是一个异步计算更新的过程。
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.addPagedListListener(mListener);
}
在创建 PagedListAdapter 实例的时候,可以通过构造参数 DiffUtil.ItemCallback 对象,在 DiffUtil.ItemCallback 中可以来实现计算的规则。
public abstract static class ItemCallback {
public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
}
我们使用 Github 的 api,实现按照指定关键字检索仓库,按照 star 数量和仓库名称降序的方式,将检索到的结果显示到 UI 上。
1. 首先使用 PageList 来批量加载数据,比如将 List 替换为 PagedList:
data class RepoSearchResult(
val data: LiveData<PagedList<Repo>>,
val networkErrors: LiveData<String>
)
当创建PagedList时,它会立即加载第一块数据,并随着时间的推移随着内容的加载而扩展。PagedList 的大小是每次传递期间装载的数据项的数目。该类既支持无限列表,也支持元素数量固定的非常大的列表。
**2. 定义 DataSource,为 PagedList 准备加载的内容。**在我们的例子中,因为数据库是UI的主要来源,所以在 Dao 中可以把 DataSource.Factory 作为返回值类型,方便创建 DataSource 实例。
@Dao
interface RepoDao {
fun reposByName(queryString: String): Factory
}
在 Repository 中通过返回的 DataSource.Factory来创建 DataSource 实例:
class GithubRepository(
private val service: GithubService,
private val cache: GithubLocalCache
) {
/**
* Search repositories whose names match the query.
*/
fun search(query: String): RepoSearchResult {
Log.d("GithubRepository", "New query: $query")
// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
// Construct the boundary callback
val boundaryCallback = RepoBoundaryCallback(query, service, cache)
val networkErrors = boundaryCallback.networkErrors
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE)
.setBoundaryCallback(boundaryCallback)
.build()
return RepoSearchResult(data, networkErrors)
}
companion object {
private const val DATABASE_PAGE_SIZE = 20
}
}
**3.配置 PagedList,**这里使用 LivePagedListBuilder 来配置,配置的内容可以包括以下内容:
4. 使用 PagedListAdapter、RecyclerView 将结果显示在 UI 上
class ReposAdapter :
PagedListAdapter<Repo, androidx.recyclerview.widget.RecyclerView.ViewHolder>(REPO_COMPARATOR) {
......
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean =
oldItem.fullName == newItem.fullName
override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean =
oldItem == newItem
}
}
}
这里的 REPO_COMPARATOR 是 DiffUtil.ItemCallback 的实现类,确定了后台计算数据更新的规则。
**5. 处理RecyclerView 滚动,实现数据的网络更新。**通过 BoundaryCallback 来实现。
class RepoBoundaryCallback(
private val query: String,
private val service: GithubService,
private val cache: GithubLocalCache
) : BoundaryCallback<Repo>() {
override fun onZeroItemsLoaded() {
requestAndSaveData(query)
}
override fun onItemAtEndLoaded(itemAtEnd: Repo) {
requestAndSaveData(query)
}
private fun requestAndSaveData(query: String) {
if (isRequestInProgress) return
isRequestInProgress = true
searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos ->
cache.insert(repos) {
lastRequestedPage++
isRequestInProgress = false
}
}, { error ->
_networkErrors.postValue(error)
isRequestInProgress = false
})
}
}
创建 BoundaryCallback 的实例,在创建 DataSource 实例时作为参数传入。
示例代码的运行效果:
完整的项目地址 示例代码地址
先简单概括一下如何使用 Paging Library:
我们再来看一下 Paging Library 的各个组成部分是如何系统工作的。
**首先,当 PagedList 创建时,**完成了两个工作:
当 PagedList 创建时,LiveData 会将 PagedList 传给 ViewModel。UI 监听到 PagedList 更新后,从 ViewModel 中取出 PagedList 传给 PagedListAdapter,最后更新在 UI 的 RecyclerView 上。这个过程如图中蓝色空心方块的运动过程。
当 PagedList 创建时,第二个工作是加载第一块数据,如果在 app 首次启动时,DataSource 中还没有数据,这时候会触发 BoundaryCallback.onZeroItemsLoaded() 方法,在我们的示例中,会从网络加载数据,并将这些数据持久化到数据库中。这个过程如图中橙色线的运动过程。
然后,当数据源中有数据后, PagedList 的新实例会被创建,这个实例最终通过 ViewModel 中的 LiveData 传到 PagedListAdapter,然后更新到 RecyclerView 上。这个过程如图中蓝色方块的运动过程。
**最后,当用户滑动屏幕触发加载下一页数据时,**如果数据源中还有可提供的数据时,重复上图中的过程。如果数据源中没有可以提供的数据,会触发 BoundaryCallback.onItemAtEndLoaded() 方法,BoundaryCallback 会从网络请求更多的数据,然后持久化到数据库中,然后根据新加载的数据,重新填充 UI。
至此,Android 架构组件 Paging 就介绍完了,下一篇我们来分析 Android 架构组件 Room 的使用。
更多内容,可以订阅 我的博客
Android Paging
应用架构指南