本文主要是介绍Android 架构组件之 Paging,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1. 为什么要使用 Paging Library?
- 2. 分析 Paging 的组成及原理
- 2.1 PagedList
- 2.2 数据源 DataSource
- 2.3 总结一下不同的数据源,如何创建 DataSource
- 2.4 PagedListAdapter
- 3. 通过一个简单的案例,介绍如何使用 Paging Library
- 4. 最后对 Paging Library 进行简单的总结
- 参考链接
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) {@Nullableprivate PagedList<Value> mList;@Nullableprivate DataSource<Key, Value> mDataSource;private final DataSource.InvalidatedCallback mCallback =new DataSource.InvalidatedCallback() {@Overridepublic void onInvalidated() {invalidate();}};@SuppressWarnings("unchecked") // for casting getLastKey to Key@Overrideprotected 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> {@Overrideboolean isContiguous() {return true;}}public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {@Overrideboolean 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 cacheval dataSourceFactory = cache.reposByName(query)// Construct the boundary callbackval boundaryCallback = RepoBoundaryCallback(query, service, cache)val networkErrors = boundaryCallback.networkErrorsval 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.fullNameoverride 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) returnisRequestInProgress = truesearchRepos(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 的使用。
更多内容,可以订阅 我的博客
参考链接
Android Paging
应用架构指南
这篇关于Android 架构组件之 Paging的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!