Paging Library 是 Google 提出的分页加载库,本文将从以下几个方面对 Paging 进行介绍:
- 为什么要使用 Paging Library?
- 分析 Paging 的组成及原理
- 通过一个简单的案例,介绍如何使用 Paging Library
- 最后对 Paging Library 进行简单的总结
1. 为什么要使用 Paging Library?
我们经常需要处理大量数据,但大多数情况下,只需要加载和显示其中的一小部分。如果去请求用户不需要的数据,势必会浪费用户设备的电量和带宽。如果数据比较多情况下,消耗用户的流量也会比较多。
Paging Library 是 Google 提出的分页加载库,它可以妥善的逐步加载数据, 解决上面提到的痛点。此外:
- Paging Library 可以与 RecyclerView 无缝结合;
- Paging Library 还支持加载有限、或无限的 List,从而使得 RecyclerView 快速,无限滚动;
- Paging Library 可以配合 LiveData、RxJava 集成使用,来观察界面中的数据变化;
- Paging Library 可以选择本地数据库,网络或两者结合的方式作为分页数据的数据源,还可以自定义如何加载内容。
Paging Library 有这么多的特点,正是我们选择的使用它的主要原因。接下来分析一下它的组成及原理。
2. 分析 Paging 的组成及原理
Paging Library 的原理是,将数据分解成多个 List,使用 RecyclerView 中的 Adapter来观察 LiveDdata 中的数据变化,在此基础上加上分页功能,从而实现逐步加载内容。
我们来看一下具体的实现过程:
看过上面Paging Library 的实现过程,我们来总结一下:
- DataSource 负责从数据源加载数据,它是连接 数据源与 PagedList 的桥梁,DataSource 的数据源可以是本地的数据库,也可以是网络,或者两者结合的方式;
- PagedList 是List 的子类,从 DataSource 中取得的数据先放到 PagedList 中,我们可以在 PagedList 中配置每次加载多少条数据;
- PagedListAdapter 是 RecyclerView.Adapter 的实现类,从 PagedList 过来的数据,经过 DiffUtil 计算出数据的差异,计算的过程是一个异步的过程;
- 计算后的数据,通过 RecyclerView.Adapter 的 onBindViewHolder() 方法,更新到 UI 上。
看过了 Paging Library 具体的执行过程,我们来分析一下它的组成。我们先来看一下 Paging Library 相关的类图。
Paging Library 的核心组件是 PagedList 和 DataSource,在上面的类图中,用不同的颜色进行了区分。下面我们分别来介绍。
2.1 PagedList
PagedList 是一个集合类,它以分块的形式异步加载数据,每一块就称为一页。
在上面的类图中,我们可以看到:
- PagedList 有四个内部类,分别是 Config、抽象类 Callback、抽象类 BoundaryCallback 和 Builder。
- Config 类可以自定义 PagedList 从数据源加载数据的一些行为,比如每页加载多少条数据 pageSize,初始加载多少数据 mInitialLoadSizeHint,是否使用占位符 mEnablePlaceholders ,预加载距离 prefetchDistance 等。通常设置 mInitialLoadSizeHint 是pageSize 的整数倍,默认是3 倍。预加载距离 prefetchDistance ,即列表当距离加载边缘多远时触发分页的请求,通常应该是屏幕上可见项的数倍,默认与 pageSize 相等。
- Callback 当数据被加载到 PagedList 中时会触发这个类中的回调方法。
- BoundaryCallback 当 PagedList 到达可用数据的末端时(需要加载分页内容时)就会触发这个类中的回调方法;
- Builder 是 PagedList 的生成器类,PagedList 的实例都是通过 Builder 类中的 build()方法产生。
在 PagedList 中,除了上面提到的这四个内部类的成员变量之外,还有两个比较重要的成员变量:
- mMainThreadExecutor 将数据传递到 Adapter 的主线程;
- mBackgroundThreadExecutor 加载数据的后台线程;
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 对象。
2.2 数据源 DataSource
再来看看 Paging Library 的另一个核心组成部分 DataSource。DataSource 是将数据加载到 PagedList 中的基类,任何数据都可以作为 DataSource 的来源,比如网络、数据库、文件等等。 DataSource.Factory 类可以用来创建 DataSource。
从上面的类图中,我们总结一下:
- DataSource 是一个抽象的泛型类,接收两个泛型参数<Key,Value>,其中 Key 表示从数据源加载数据项的唯一标识,Value 与标识 Key 对应的数据项。DataSource 中定义了一个抽象的静态内部类 Factory<Key, Value>,是创建 DataSource 的工厂类。
- DataSource 有两个直接的子类,分别是 PositionalDataSource 和 ContiguousDataSource。ContiguousDataSource 是一个非 public 的类,我们通常使用它的两个子类,PageKeyedDataSource 和 ItemKeyedDataSource 。
- PositionalDataSource 和 ContiguousDataSource 这两个类最大的区别是对抽象方法 isContiguous() 的实现方式不同,PositionalDataSource 中的 isContiguous() 方法返回 false,ContiguousDataSource 中的 isContiguous() 方法返回 true。所以我们在代码中,能够使用的 DataSource 有三种,分别是 PositionalDataSource 和 ContiguousDataSource 的两个子类 PageKeyedDataSource 和 ItemKeyedDataSource 。
public abstract class DataSource<Key, Value> {
/**
* Returns true if the data source guaranteed to produce a contiguous set of items,
* never producing gaps.
*/
abstract boolean isContiguous();
}
abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
@Override
boolean isContiguous() {
return true;
}
}
public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
@Override
boolean isContiguous() {
return false;
}
}
关于数据源产生的数据项是否为连续的,结合后面三种 DataSource 的使用场景更好理解。
我们来看一下 PositionalDataSource、PageKeyedDataSource 和 ItemKeyedDataSource 分别适用哪些场景:
- 使用 PositionalDataSource,需要我们的实现类实现 loadInitial() 和 loadRange() 方法,适用于数据项总数固定,要通过特定的位置加载数据。比如从某个位置开始的 100 条数据;
- 使用 PageKeyedDataSource,需要实现 loadInitial()、loadBefore() 和 loadAfter() 方法,适用于以页信息加载数据的场景。比如在网络加载数据的时候,需要通过 setNextKey() 和 setPreviousKey() 方法设置下一页和上一页的标识 Key。
- 使用 ItemKeyedDataSource 除了需要实现 loadInitial()、loadBefore() 和 loadAfter() 方法以外,还要实现getKey() 方法,适用于所加载的数据依赖其他现有数据信息的场景。比如要加载的下一页的数据,依赖于当前页的数据。
2.3 总结一下不同的数据源,如何创建 DataSource
假设数据源是数据库,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。
2.4 PagedListAdapter
Paging Library 提供了 PagedListAdapter,可以将 PagedList 中的数据加载到 RecyclerView 中,PagedListAdapter 会在页加载时收到通知,收到新数据时,会使用 DiffUtil 精细计算更新。
在 PagedListAdapter 中使用的是 AsyncPagedListDiffer,从名字就能看出这是一个异步计算更新的过程。
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.addPagedListListener(mListener);
}
在创建 PagedListAdapter 实例的时候,可以通过构造参数 DiffUtil.ItemCallback 对象,在 DiffUtil.ItemCallback 中可以来实现计算的规则。
public abstract static class ItemCallback<T> {
public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
}
3. 通过一个简单的案例,介绍如何使用 Paging Library
我们使用 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<Int, Repo>
}
在 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 来配置,配置的内容可以包括以下内容:
- 由 PagedList 加载的页面的大小;
- 加载的距离;
- 第一次加载时要加载多少项;
- 是否可以将空项添加到PagedList中,以表示尚未加载的数据。
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 实例时作为参数传入。
示例代码的运行效果:
更多内容,可以订阅 我的博客
完整的项目地址 示例代码地址
4. 最后对 Paging Library 进行简单的总结
先简单概括一下如何使用 Paging Library:
- 首先,要定义 DataSource;
- 需要的时候,创建 BoundaryCallback;
- 使用LivePagedListBuilder创建 PagedList 的 LiveData;
- 将 Adapter 转化为 PagedListAdapter,
- 最后在 UI 中观察 PagedList 的 LiveData 对象,并将 更新后的PagedList 传给 PagedListAdapter。
我们再来看一下 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 的使用。
更多内容,可以订阅 我的博客