Paging简介
Paging主要由三个部分组成:DataSource PageList PageListAdapter。
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条数据。
测试例子
PagedList配置如下
private PagedList.Config config = new PagedList.Config.Builder()
.setInitialLoadSizeHint(10) // 配置 初始化 加载条数
.setPageSize(10) // 设置 每页 加载条数
.setPrefetchDistance(2)
.setMaxSize(20)
.setEnablePlaceholders(false)
.build();
private LiveData<PagedList<Cheese>> livePagedList = new LivePagedListBuilder<Integer,Cheese>(factory,config)
.setInitialLoadKey(0)// 指定初始化加载的key
// .setFetchExecutor()// 指定线程执行异步操作
.setBoundaryCallback(new CheeseBoundaryCallback())
.build();
场景描述:
数据库默认有100条数据。初始化加载0~10条数据,PagedList最多存储20条数据。在滑动在距离上边缘2条和下边缘2条的时候加载更多数据。
运行现象:
滑动过程中容易出现只能加载20条数据,超过20条的数据无法加载。
解决方案
private PagedList.Config config = new PagedList.Config.Builder()
.setInitialLoadSizeHint(10) // 配置 初始化 加载条数
.setPageSize(10) // 设置 每页 加载条数
.setPrefetchDistance(2)
.setMaxSize(10 *3) // 配置pagedList最大加载条数为3倍的PageSize
.setEnablePlaceholders(false)
.build();
关于解决方案的思路是:思路是保证loadBefore,loadInitial,loadAfter三个回调获取到的数据都可以完整的保存在PageList里面。
初始化流程简介
由于遇到了以上的问题,因此,对Paging如果更新RecyclerView,已经实现加载更多的流程有了兴趣,因此,对代码分析如下。
使用PagedList一般是将PagedList存储在ViewHolder中,因此ViewHolder的代码一般如下
public class CheeseViewModel extends ViewModel{
private LiveData<PagedList<Cheese>> livePagedList = new LivePagedListBuilder<Integer,Cheese>(factory,config)
.setInitialLoadKey(0)// 指定初始化加载的key
// .setFetchExecutor()// 指定线程执行异步操作
.setBoundaryCallback(new CheeseBoundaryCallback())
.build();
}
根据源码可以得知,最终调用的代码是
static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key){
...
if (dataSource.isContiguous() || !config.enablePlaceholders){
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
}
}
在ContiguousPagedList构造函数中调用dispatchLoadInitial方法。
dispatchLoadInitial 即回调到ItemKeyedDataSource的 dispatchLoadInitial 函数中
public abstract class ItemKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value>{
@Override
final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
LoadInitialCallbackImpl<Value> callback =
new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
// 注意 loadInitial 是抽象方法,需要在子类实现
loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
}
}
抽象方法在继承ItemKeyedDataSource的子类实现,源码如下
public class MyItemKeyedDataSource extends ItemKeyedDataSource<Integer,Cheese>{
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) {
...
// 获取cheeseList,通过回调传递给ItemKeyedDataSource
callback.onResult(cheeseList);
}
}
LoadInitialCallback 在ItemKeyedDataSource的实现如下
public abstract class ItemKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value>{
static class LoadInitialCallbackImpl<Value> extends LoadInitialCallback<Value> {
public void onResult(@NonNull List<Value> data){
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
// 调用核心的Receiver
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
}
}
核心的ContiguousPagedList#mReceiver如下,PageResult.Receiver实现RecyclerView item的初始化已经加载更新。
class ContiguousPagedList<K, V> extends PagedList<V> implements Callback{
PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
// Creation thread for initial synchronous load, otherwise main thread
// Safe to access main thread only state - no other thread has reference during construction
@AnyThread
@Override
public void onPageResult(@PageResult.ResultType int resultType,
@NonNull PageResult<V> pageResult) {
if (pageResult.isInvalid()) {
detach();
return;
}
if (isDetached()) {
// No op, have detached
return;
}
List<V> page = pageResult.page;
if (resultType == PageResult.INIT) {
mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
pageResult.positionOffset, ContiguousPagedList.this);
if (mLastLoad == LAST_LOAD_UNSPECIFIED) {
// Because the ContiguousPagedList wasn't initialized with a last load position,
// initialize it to the middle of the initial load
mLastLoad =
pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
}
} else {
// if we end up trimming, we trim from side that's furthest from most recent access
boolean trimFromFront = mLastLoad > mStorage.getMiddleOfLoadedRange();
// is the new page big enough to warrant pre-trimming (i.e. dropping) it?
boolean skipNewPage = mShouldTrim
&& mStorage.shouldPreTrimNewPage(
mConfig.maxSize, mRequiredRemainder, page.size());
if (resultType == PageResult.APPEND) {
if (skipNewPage && !trimFromFront) {
// don't append this data, drop it
mAppendItemsRequested = 0;
mAppendWorkerState = READY_TO_FETCH;
} else {
mStorage.appendPage(page, ContiguousPagedList.this);
}
} else if (resultType == PageResult.PREPEND) {
if (skipNewPage && trimFromFront) {
// don't append this data, drop it
mPrependItemsRequested = 0;
mPrependWorkerState = READY_TO_FETCH;
} else {
mStorage.prependPage(page, ContiguousPagedList.this);
}
} else {
throw new IllegalArgumentException("unexpected resultType " + resultType);
}
if (mShouldTrim) {
if (trimFromFront) {
if (mPrependWorkerState != FETCHING) {
if (mStorage.trimFromFront(
mReplacePagesWithNulls,
mConfig.maxSize,
mRequiredRemainder,
ContiguousPagedList.this)) {
// trimmed from front, ensure we can fetch in that dir
mPrependWorkerState = READY_TO_FETCH;
}
}
} else {
if (mAppendWorkerState != FETCHING) {
if (mStorage.trimFromEnd(
mReplacePagesWithNulls,
mConfig.maxSize,
mRequiredRemainder,
ContiguousPagedList.this)) {
mAppendWorkerState = READY_TO_FETCH;
}
}
}
}
}
if (mBoundaryCallback != null) {
boolean deferEmpty = mStorage.size() == 0;
boolean deferBegin = !deferEmpty
&& resultType == PageResult.PREPEND
&& pageResult.page.size() == 0;
boolean deferEnd = !deferEmpty
&& resultType == PageResult.APPEND
&& pageResult.page.size() == 0;
deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
}
}
};
}
由于是初始化,因此resultType == PageResult.INIT
执行的分支并未调用postValue接口,更新数据,只是将数据保存在agedStorage mStorage 中。
if (resultType == PageResult.INIT) {
mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
pageResult.positionOffset, ContiguousPagedList.this);
if (mLastLoad == LAST_LOAD_UNSPECIFIED) {
// Because the ContiguousPagedList wasn't initialized with a last load position,
// initialize it to the middle of the initial load
mLastLoad =
pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
}
}
这时候需要查看源码LivePagedListBuilder,ViewHolder持有的LiveData本质是ComputableLiveData,在第一次订阅观察者是,会调用onActive
public ComputableLiveData(@NonNull Executor executor) {
mExecutor = executor;
mLiveData = new LiveData<T>() {
@Override
protected void onActive() {
mExecutor.execute(mRefreshRunnable);
}
};
}
onActive执行mRefreshRunnable线程,
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
boolean computed;
do {
computed = false;
// compute can happen only in 1 thread but no reason to lock others.
if (mComputing.compareAndSet(false, true)) {
// as long as it is invalid, keep computing.
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
value = compute();
}
if (computed) {
// 这里调用postValue接口,这时候在生命周期允许的条件下回调更新UI
mLiveData.postValue(value);
}
} finally {
mComputing.set(false);
}
}
scenario.
} while (computed && mInvalid.get());
}
};
Activity或者Fragment中注册PagedList的观察者代码如下
viewModel.getLivePagedList().observe(getViewLifecycleOwner(),
cheeses ->{
Log.d(TAG,"===submitList==="+cheeses.size());
// 这里最终调用adapter的更新接口,更新UI
adapter.submitList(cheeses);
}
);
以上即为使用PagedListAdapter初始化RecyclerView的流程。
滑动更新
RecyclerView在创建Item过程中,会调用onCreateViewHolder和onBindViewHolder等接口,在onBindViewHolder接口中,需要获取Adapter的Item列表,用于与item界面元素绑定。
public class CheeseAdapter extends PagedListAdapter<Cheese,CheeseViewHolder> {
@Override
public void onBindViewHolder(@NonNull CheeseViewHolder holder, int position) {
holder.bindTo(getItem(position));
}
}
getItem最终调用到
public class AsyncPagedListDiffer{
public T getItem(int index) {
…
// 这个方法实现了滑动刷新
mPagedList.loadAround(index);
return mPagedList.get(index);
}
}
protected void loadAroundInternal(int index) {
int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount());
int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount() + mStorage.getStorageCount());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
if (mPrependItemsRequested > 0) {
// 调用 loadBefore
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
// loadAfter
scheduleAppend();
}
}