目录:
需求来源
Paging运作流程
Paging三大部件
如何使用
三种DataSource对比
LivePagedListBuilder、RxPagedListBuilder对比
一、需求来源:
为方便实现上拉加载、简化代码、上拉加载逻辑可配置...总之就是为了方便
Paging
出现前,上拉加载触发一般是通过:
- 监听RecyclerView的滚动事件,判断RecyclerView是否滚动到底部
- 处理Adapter的
onBindViewHolder
方法,根据位置与数量判断当前位置item是否该触发上拉加载 - 或者其他方式...
如方式一:
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var shouldReload = false
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE && shouldReload) {
// 加载下一页
...
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager: LinearLayoutManager =
recycler_view.layoutManager as LinearLayoutManager
layoutManager.apply {
val firstVisibleItem = findFirstVisibleItemPosition()
val visibleItemCount = childCount
val totalItemCount = itemCount
shouldReload = firstVisibleItem + visibleItemCount == totalItemCount && dy > 0
}
}
})
BaseRecyclerViewAdapterHelper库实现方式为方案二,部分代码如下:
@Override
public void onBindViewHolder(@NonNull K holder, int position) {
... // 其他代码
autoLoadMore(position);
... // 其他代码
}
@Override
public void onBindViewHolder(@NonNull K holder, int position, @NonNull List<Object> payloads) {
... // 其他代码
autoLoadMore(position);
... // 其他代码
}
// 自动上拉加载判断逻辑
private void autoLoadMore(int position) {
// mPreLoadNumber为预加载控制数量,默认为1
if (position < getItemCount() - mPreLoadNumber) {
return;
}
if (mLoadMoreView.getLoadMoreStatus() != LoadMoreView.STATUS_DEFAULT) {
return;
}
... // 其他代码
mRequestLoadMoreListener.onLoadMoreRequested();
... // 其他代码
}
Paging运作流程
首先,Adapter会继承自PagedListAdapter(DiffUtils)
,PagedListAdapter(DiffUtils)
最终也是继承自RecyclerView.Adapter
。执行到onBindViewHolder()
数据绑定时,调用getItem(position)
获取当前位置的数据,此时PagedListAdapter
就会知道列表现在处于什么位置,以及是否触发加载下一页功能。当加载下一页功能被触发时,会通知内部PagedList
向DataSource
拉取数据,获得数据后通过DiffUtils
对比得到最终数据集
Paging三大部件
1. PagedListAdapter
继承自RecyclerView.Adapter
,用来承载PagedList。PagedListAdapter
构造函数需提供DiffUtil.ItemCallback
对象,或者是AsyncDifferConfig
对象。
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.addPagedListListener(mListener);
}
protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addPagedListListener(mListener);
}
数据对比主要是用实例化的AsyncDifferConfig
对象,其构造方法如下:
public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mUpdateCallback = new AdapterListUpdateCallback(adapter);
mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
}
@SuppressWarnings("WeakerAccess")
public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
@NonNull AsyncDifferConfig<T> config) {
mUpdateCallback = listUpdateCallback;
mConfig = config;
}
2. PagedList
PagedList
是一个抽象类,是一个List的封装类别,与我们熟悉的ArrayList
一样继承自AbstractList
。用来存储分页载入的数据集合,并且通知DataSource
数据加载时机。可以通过PagedList.Config
配置
(1). mPageSize:页面大小即每次加载时加载的数量
(2). mPrefetDistance:预取距离,给定UI中最后一个可见的Item,超过这个Item应该预取一段数据
3. DataSource
负责实现数据载入实现,常用子类如下:
-
PageKeyedDataSource
:当后一页的取得方式从当前页得知,通过当前页相关的key来获取数据。
假定一个场景帮助理解:浏览短视频数据时返回的每个视频携带视频类别,当哪个视频观看比较久,则下一页优先获取该类别的视频--后一页的请求,根据前一页的key(视频类别)获取
简单常见的按照页序号查询数据也可以用这个DataSource。初始时callback设置前后页key为-1,1。加载下一页设置callback的key递增1,加载上一页设置callback的key递减1.
-
PositionalDataSource
:通过在数据中的position作为key来获取下一页数据。数据排一排,根据配置的起始位置和获取数量,获取从指定位置开始后续n条数据。比如说,请求返回从第100条数据开始的之后20条数据。
假如设置初始请求从第30项开始,获取15条数据。此时下一页请求参数为从第45项开始,获取15条数据。参数中startPosition=requestedStartPosition+pageSize
-
ItemKeyedDataSource
:通过当前页数据信息(具体的item)作为key,来获取下一页数据。必须由当前页数据去加载下一页数据。
场景:例如小红书、资讯类的app,上下页的请求参数都是从当前页配置的
如何使用
1. 定义DiffCallback
boolean areItemsTheSame(@NonNull DataBean oldItem, @NonNull DataBean newItem)
:判断是不是同一个Item
boolean areContentsTheSame(@NonNull DataBean oldItem, @NonNull DataBean newItem)
:判断两个Item内容是否相同,当areItemsTheSame
为true时才调用
2. 初始化适配器,绑定RecyclerView
使用基本与RecyclerView.Adapter
一样
差异点:需传入
DiffUtil.ItemCallback
。获取数据使用getItem(position)
方法
private class MyAdapter extends PagedListAdapter<DataBean, ViewHolder> {
public MyAdapter() {
super(mDiffCallback);
}
... // 其他代码
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
DataBean data = getItem(position);
... // 填充Item
}
}
3. 设置PagedList.Config
PagedList.Config mPagedListConfig = new PagedList.Config.Builder()
.setPageSize(2) // 设置后续页面(非第一页)每页加载的数量
.setPrefetchDistance(2) // getItem(position)时调用
.setEnablePlaceholders(true) // 设置占位符,默认true
.setInitialLoadSizeHint(3) // 设置第一页加载的数量
.build();
4. 设置DataSource
private class MyDataSource extends PageKeyedDataSource<Integer, DataBean> {
@Override
public void loadInitial(@NonNull final LoadInitialParams<Integer> params,
@NonNull final LoadInitialCallback<Integer, DataBean> callback) {
// 请求第一页数据
}
@Override
public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, DataBean> callback) {
}
@Override
public void loadAfter(@NonNull final LoadParams<Integer> params,
@NonNull final LoadCallback<Integer, DataBean> callback) {
// 请求下一页数据
}
}
5. 设置DataSourceFactory
private class DataSourceFactory extends DataSource.Factory<Integer, DataBean> {
@NonNull
@Override
public DataSource<Integer, DataBean> create() {
return new MyDataSource();
}
}
6. 设置LiveData
LivePagedListBuilder<Integer, DataBean> builder = new LivePagedListBuilder<>(
new DataSourceFactory(), mPagedListConfig);
LiveData<PagedList<DataBean>> mPagedList = builder.build();
mPagedList.observe(this, new Observer<PagedList<DataBean>>() {
@Override
public void onChanged(PagedList<DataBean> o) {
// 填充数据
mAdapter.submitList(o);
}
});
三种DataSource
对比
1. PositionalDataSource
:根据列表的绝对位置获取数据
/**
* desc:根据列表的绝对位置决定放数据</br>
* 从任意指定位置开始获取数据
* time: 2019/11/12-11:25</br>
* author:Leo </br>
*/
注:其中的泛型Item是请求到列表每项数据的实体类
class TestPositionalDataSource : PositionalDataSource<Item>() {
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Item>) {
params.apply {
val items = getIncreaseItems(startPosition, loadSize)
callback.onResult(items)
}
}
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Item>) {
params.apply {
val items = getIncreaseItems(requestedStartPosition, pageSize)
callback.onResult(items, 0)
}
}
}
该方式的关键点在于LoadInitialParams
和LoadRangeParams
中参数的获取
ContiguousPagedList.java
175行 ContiguousPagedList构造函数
mDataSource.dispatchLoadInitial(key,
mConfig.initialLoadSizeHint,
mConfig.pageSize,
mConfig.enablePlaceholders,
mMainThreadExecutor,
mReceiver);
- LoadInitialParams各参数与PagedList.Config对应关系如下:
// key必须为Integer类型
requestedStartPosition -> LivePagedListBuilder.setInitialLoadKey
requestedLoadSize -> config.initialLoadSizeHint
pageSize -> config.pageSize
placeholdersEnabled -> false // 该模式下写死为false
PositionalDataSource.java
final void dispatchLoadInitial(...) {
...
LoadInitialParams params = new LoadInitialParams(
requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
loadInitial(params, callback);
...
}
初次加载数据时,参数值读取的参数如上构造函数`LoadInitialParams`
注意:此时placeholdersEnabled实际为false
LoadRangeParams参数值来源
- 取下一页 dispatchLoadAfter
startPosition -> 当前位置+1
loadSize -> config.pageSize- 取上一页 dispatchLoadBefore
参数值根据不同情景赋值,源码如下
PositionalDataSource.java
@Override
void dispatchLoadBefore(...) {
int startIndex = currentBeginIndex - 1;
if (startIndex < 0) {
// 列表为空 情景一
mSource.dispatchLoadRange(
PageResult.PREPEND, startIndex, 0, mainThreadExecutor,receiver);
} else {
// 列表不为空 情景二
int loadSize = Math.min(pageSize, startIndex + 1);
startIndex = startIndex - loadSize + 1;
mSource.dispatchLoadRange(
PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
}
}
LoadRangeParams参数值来源
情景一:此时列表为空,此时
startPosition -> -1
loadSize -> 0
情景二:正常加载之前数据
startPosition -> 当前位置向前推loadSize个数
loadSize -> 当前位置与config.pageSize的最小者(避免可能出现当前位置之前的数量少于pageSize情况)
总结:初始化加载时,参数从PagedList配置项读取,无占位符。加载前后数据时,开始位置根据当前位置向前/后推算,加载数量读取自
config.pageSize
。该类型DataSource
无法加载上一页
- 此时获取上页/下页数据仅仅根据位置序号就能获得。
使用注意点:
使用PositionalDataSource时,需注意需设置config.enablePlaceholders=false,否则会崩溃。原因详解如下:
PagedList.class
@NonNull
static <K, T> PagedList<T> create(...) {
/**
* PositionalDataSource时,dataSource.isContiguous()恒等于false
* 此时config.enablePlaceholders=false才执行if逻辑
**/
if (dataSource.isContiguous() || !config.enablePlaceholders) {
...其他代码
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor, fetchExecutor,
boundaryCallback, config, key, lastLoad);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
notifyExecutor, fetchExecutor, boundaryCallback,
config, (key != null) ? (Integer) key : 0);
}
}
create方法生成的ContiguousPagedList、TiledPagedList对象最终都会调用PositionalDataSource.dispatchLoadInitial方法,如下:
final void dispatchLoadInitial(...) {
/**
* 主要生成LoadInitialCallbackImpl对象
* ContiguousPagedList方式调用时传的acceptCount为false,TiledPagedList传的为true
**/
LoadInitialCallbackImpl<T> callback =
new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
... 其他代码
}
继续看LoadInitialCallbackImpl代码:
static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
... // 其他代码
LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled, int pageSize, PageResult.Receiver<T> receiver) {
... // 其他代码
// ContiguousPagedList方式调用时传的acceptCount为false,TiledPagedList传的为true
mCountingEnabled = countingEnabled;
... // 其他代码
}
@Override
public void onResult(@NonNull List<T> data, int position, int totalCount) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
... // 其他代码
if (mCountingEnabled) {
int trailingUnloadedCount = totalCount - position - data.size();
mCallbackHelper.dispatchResultToReceiver(
new PageResult<>(data, position, trailingUnloadedCount, 0));
}
... // 其他代码
}
}
@Override
public void onResult(@NonNull List<T> data, int position) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
... // 其他代码
if (mCountingEnabled) {
throw new IllegalStateException("Placeholders requested, but totalCount not"
+ " provided. Please call the three-parameter onResult method, or"
+ " disable placeholders in the PagedList.Config");
}
... // 其他代码
}
}
}
从onResult方法代码可知mCountingEnabled为true一定会抛出异常。
所以在PagedList.create方法中需保证不创建TiledPagedList对象,即设置config.enablePlaceholder=false
2. PageKeyedDataSource
:原始数据已有分页功能,根据每页的Key取得上下页数据
/**
* desc:原始数据已有分页功能,根据每页的Key取得数据</br>
* time: 2019/11/12-11:25</br>
* author:Leo </br>
*/
注:其中的泛型Int是用于请求上下页数据的参数key实体类型
其中的泛型Item是请求到列表每项数据的实体类
class TestPageKeyedDataSource : PageKeyedDataSource<Int, Item>() {
override fun loadInitial(
params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>
) {
val items = getItems(params.requestedLoadSize)
callback.onResult(items, items[0].id, items.last().id)
}
// 这里的参数key实际就是上述的items.last().id
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
params.apply {
val items = getIncreaseItems(key, requestedLoadSize)
callback.onResult(items, items.last().id)
}
}
// 这里的参数key实际就是上述的items[0].id
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
params.apply {
val items = getReduceItems(key, requestedLoadSize)
callback.onResult(items, items.first().id)
}
}
}
LoadInitialParams参数与PositionalDataSource
一致。但构造函数只取了requestedLoadSize
和placeholdersEnabled
这两个参数。调用代码如下:
PageKeyedDataSource.java
@Override
final void dispatchLoadInitial(...) {
LoadInitialCallbackImpl<Key, Value> callback =
new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);
...
}
注意:该类型DataSource,参数值只取了`requestedLoadSize`和`placeholdersEnabled`这两个参数
LoadParams包含两个参数key(上页/下页的key)
requestedLoadSize(需要加载的数量)
requestedLoadSize
:即为config.pageSize
key
:的赋值/初始化时机都在上述TestPageKeyedDataSource
类的几个callback.onResult(...)方法中
参数key的赋值:
@Override
final void dispatchLoadAfter(...) {
@Nullable Key key = getNextKey();
...
loadAfter(new LoadParams<>(key, pageSize),...);
...
}
@Override
final void dispatchLoadBefore(...) {
@Nullable Key key = getPreviousKey();
...
loadBefore(new LoadParams<>(key, pageSize),...);
...
}
上下页key的赋值,关键代码如下:
1. void initKeys(@Nullable Key previousKey, @Nullable Key nextKey)
2. void setPreviousKey(@Nullable Key previousKey)
3. void setNextKey(@Nullable Key nextKey)
initKeys方法在LoadInitialCallback.onResult(...)中。精简代码如下
static class LoadInitialCallbackImpl<Key, Value> extends LoadInitialCallback<Key, Value> {
...
LoadInitialCallbackImpl(...) {
...
}
@Override
public void onResult(@NonNull List<Value> data, int position, int totalCount,
@Nullable Key previousPageKey, @Nullable Key nextPageKey) {
...
// setup keys before dispatching data, so guaranteed to be ready
mDataSource.initKeys(previousPageKey, nextPageKey);
...
}
@Override
public void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey) {
...
mDataSource.initKeys(previousPageKey, nextPageKey);
...
}
}
static class LoadCallbackImpl<Key, Value> extends LoadCallback<Key, Value> {
...
@Override
public void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey) {
...
if (mCallbackHelper.mResultType == PageResult.APPEND) {
mDataSource.setNextKey(adjacentPageKey);
} else {
mDataSource.setPreviousKey(adjacentPageKey);
}
...
}
}
总结:初始化加载时,会设置上页/下页的关键值作为获取上页/下页数据的参数。每页需加载的数量从config.pageSize获取
- 每页数据都返回两个参数:获取上页数据的key,获取下页数据的key。且获取每页数据时必须传入key才能请求到该页数据
- 只要正确配置
previousPageKey
和nextPageKey
(上述代码中的items[0].id、items.last().id),上下滑动都可以加载数据,在第一页时也能滑动
3. ItemKeyedDataSource
:当列表数据的Key有连续性,可根据Key找到下一页或上一页数据
/**
* desc:当列表数据的Key有连续性,可根据Key找到下一页或上一页数据</br>
* time: 2019/11/12-11:24</br>
* author:Leo </br>
*/
注:泛型Int即为getKey()方法返回的类型
泛型Item为列表各项的实体类型
class TestItemKeyedDataSource : ItemKeyedDataSource<Int, Item>() {
// 因为每页请求都需要key,所以需配置一个初始key-requestedInitialKey
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Item>) {
params.apply {
callback.onResult(getIncreaseItems(requestedInitialKey ?: 0, requestedLoadSize))
}
}
// 这里的key是适配器中数据集最后一项数据的item.id
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Item>) {
params.apply {
callback.onResult(getIncreaseItems(key + 1, requestedLoadSize))
}
}
// 这里的key是适配器中数据集第一项数据的item.id
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Item>) {
params.apply {
callback.onResult(getIncreaseItems(key, requestedLoadSize))
}
}
override fun getKey(item: Item) = item.id
}
LoadInitialParams参数与上述两类型参数相同,但构造方法有点区别,精简源码如下:
ItemKeyedDataSource.java
@Override
final void dispatchLoadInitial(...) {
...
loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
...
}
LoadInitialParams各参数与PagedList.Config对应关系如下:
requestedStartPosition -> LivePagedListBuilder.setInitialLoadKey()
requestedLoadSize -> config.initialLoadSizeHint
placeholdersEnabled -> config.enablePlaceholders
注:初次加载数据不需指定加载数据pageSize
LoadParams包含两个参数key(上页/下页的key)
requestedLoadSize(需要加载的数量)
requestedLoadSize
:即为config.pageSize
key
:该key一般都为请求返回实体类中的一个字段,父类根据上页/下页拿到缓存下集合中的第一个或最后一个数据实体。然后根据子类中重写的getKey(item: T)
方法指定key所对应的字段。部分代码如下:
ItemKeyedDataSource.java
@Override
final void dispatchLoadAfter(...) {
loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize),...);
}
@Override
final void dispatchLoadBefore(...) {
loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),...);
}
getKey(currentEndItem)、getKey(currentBeginItem)。getKey为抽象方法,
子类重写指定对应字段,如下:
TestItemKeyedDataSource.java
override fun getKey(item: Item) = item.id
总结:初始化加载时,根据config配置加载当前页数据。渲染数据的同时也会缓存到Storage中。当获取上页/下页数据时,取得Storage中的第一条/最后一条数据。再通过子类重写的getKey方法,拿到请求所需实际的key。
LivePagedListBuilder、RxPagedListBuilder对比
从两个点对比:使用方式,值回调方式
- 如何使用
/**
* 最终返回的是 LiveData<PagedList<T>>类型对象
*/
fun allTestLiveData(id: String) = this.let {
LivePagedListBuilder(
createFactory(id), PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(13)
.setEnablePlaceholders(false)
.setPrefetchDistance(3).build()
).build()
}.observe(this, Observer { adapter.submitList(it) })
/**
* 最终返回的是 Flowable<PagedList<T>>类型对象
*/
fun allTestFlowable(id: String) = this.let {
RxPagedListBuilder(
createFactory(id), PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(13)
.setEnablePlaceholders(false)
.setPrefetchDistance(3).build()
).buildFlowable(BackpressureStrategy.DROP)
}.subscribe { adapter.submitList(it) }
- 值回调方式
两种Builder一个为LiveData,一个为RxJava。
先看第一个LivePagedListBuilder
。
LivePagedListBuilder通过build方法构造了一个ComputableLiveData
对象,最终会执行一个Runnable,看代码:
ComputableLiveDat.java
@VisibleForTesting
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
boolean computed;
do {
... // 其他代码
mLiveData.postValue(value);
... // 其他代码
} while (computed && mInvalid.get());
}
};
关键代码为postValue(value),通过LiveData的postValue方法设置数据,并在外部监听
对于RxPagedListBuilder
,通过buildObservable方法创建一个Observable,将其他逻辑代码放到一个ObservableOnSubscribe类中实现,subscribe回调方法中实现PageList的构造,代码如下:
private PagedList<Value> createPagedList() {
... // 其他代码
do {
... // 其他代码
mList = new PagedList.Builder<>(mDataSource, mConfig)
.setNotifyExecutor(mNotifyExecutor)
.setFetchExecutor(mFetchExecutor)
.setBoundaryCallback(mBoundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());
return mList;
}
最终结果还是为了构造一个PagedList。
两种方式最终都是为了构造一个PagedList,一个通过ComputableLiveData
包裹一层通过在Runnable中调用LiveData的postValue方法通知值更新;另一个是借助RxJava的线程切换,创建后通过设置观察者取得结果。
参考文章:
https://enginebai.com/2019/04/22/android-paging-part1/