Q:什么是Paging?
Paging 库提供了列表中分页数据加载的解决方案,可帮助您加载和显示来自本地存储或网络中更大的数据集中的数据页面。此方法可让您的应用更高效地利用网络带宽和系统资源。
Q:谈谈使用Paging的好处?
- 分页数据缓存到内存中,保证应用在处理页面数据的时候,更有效的使用系统资源
- 同时多个相同的请求只会触发一个,确保App有效的使用网络资源和系统资源
- 可以配置RecyclerView的adapters,让其滑动到末尾自动发起请求
- 对Kotlin协程和Flow以及LiveData、RxJava 有很好的支持
- 内置刷新、重试、错误处理等功能
Q:Paging是如何初始化数据的?即何时请求的第一页的数据?
我们通过LiveData来对PagedList中的数据进行监听,LiveData<PagedList>是通过LivePagedListBuilder.build()得到的,在build()方法中调用了其的create()方法,该方法返回ComputableLiveData<PagedList<>>对象,并把拉取数据的线程池传入ComputableLiveData对象,同时调用其的getLiveData()返回它所包装的一个LiveData。
private static <Key, Value> LiveData<PagedList<Value>> create(
@Nullable final Key initialLoadKey,//ItemKeyedDataSource会用到
@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();
}
};
@Override
protected PagedList<Value> compute() {
@Nullable Key initializeKey = initialLoadKey;
if (mList != null) {
//noinspection unchecked
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();
}
在这个ComputableLiveData对象初始化时,也初始化了一个LiveData对象,并在它的onActive()方法中在传进来的线程池执行了mRefreshRunnable任务,
public ComputableLiveData(@NonNull Executor executor) {
mExecutor = executor;
mLiveData = new LiveData<T>() {
@Override
protected void onActive() {
mExecutor.execute(mRefreshRunnable);
}
};
}
在该Runnable中调用了Computable的compute()方法,compute()方法通过传入的dataSourceFactory创建DataSource以及PagedList对象,实际是PagedList子类ContiguousPagedList的实例,在ContiguousPagedList的构造器中,将会调用此前创建的DataSource的dispatchLoadInitial(),进而调用loadInitial(),开始请求第一页的数据(PageKeyedDataSource)
@Override
final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
LoadInitialCallbackImpl<Key, Value> callback =
new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);
// If initialLoad's callback is not called within the body, we force any following calls
// to post to the UI thread. This constructor may be run on a background thread, but
// after constructor, mutation must happen on UI thread.
callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
}
Q:Paging得到数据后如何与界面交互?即数据更新是如何调用Adapter去更新UI的?
数据请求成功之后,会调用到callback对象的result()方法,
@Override
public void loadInitial(@NonNull LoadInitialParams<K> params,
final @NonNull LoadInitialCallback<K, B> callback) {
mSource.loadInitial(params, new LoadInitialCallback<K, A>() {
@Override
public void onResult(@NonNull List<A> data, int position, int totalCount,
@Nullable K previousPageKey, @Nullable K nextPageKey) {
callback.onResult(convert(mListFunction, data), position, totalCount,
previousPageKey, nextPageKey);
}
@Override
public void onResult(@NonNull List<A> data, @Nullable K previousPageKey,
@Nullable K nextPageKey) {
callback.onResult(convert(mListFunction, data), previousPageKey, nextPageKey);
}
});
}
在onResult()方法里面最终调用的是PageResult.Receiver的onPageResult方法
@Override
public void onResult(@NonNull List<Value> data, int position, int totalCount,
@Nullable Key previousPageKey, @Nullable Key nextPageKey) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
// setup keys before dispatching data, so guaranteed to be ready
mDataSource.initKeys(previousPageKey, nextPageKey);
int trailingUnloadedCount = totalCount - position - data.size();
if (mCountingEnabled) {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
data, position, trailingUnloadedCount, 0));
} else {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
}
void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
Executor executor;
synchronized (mSignalLock) {
if (mHasSignalled) {
throw new IllegalStateException(
"callback.onResult already called, cannot call again.");
}
mHasSignalled = true;
executor = mPostExecutor;
}
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
mReceiver.onPageResult(mResultType, result);
}
});
} else {
mReceiver.onPageResult(mResultType, result);
}
}
PageResult.Receiver是一个抽象类,在ContiguousPagedList实现并传递过去的,这里就来看下这里面做了什么:
private 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 (resultType == PageResult.APPEND) {
//加载初始化之前的数据会执行
mStorage.appendPage(page, ContiguousPagedList.this);
} else if (resultType == PageResult.PREPEND) {
//加载初始化之后的数据会执行
mStorage.prependPage(page, ContiguousPagedList.this);
} else {
throw new IllegalArgumentException("unexpected resultType " + resultType);
}
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);
}
}
};
返回的数据在这里取出来了,并传递到PagedStorage对象的init()中去了,调用到了callback的onInitialized()方法,这个方法在ContiguousPagedList有实现:
public void onInitialized(int count) {
notifyInserted(0, count);
}
很简单就是调用了他自己的notifyInserted()方法,
void notifyInserted(int position, int count) {
if (count != 0) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback callback = mCallbacks.get(i).get();
if (callback != null) {
callback.onInserted(position, count);
}
}
}
}
还记得PagedListAdapter的submitList么,对了,就是在这个方法里面添加的回调,PagedListAdapter里面使用的是代理模式,实际的功能是AsyncPagedListDiffer来处理的,所以这里就来看看AsyncPagedListDiffer的submitList()方法:
public void submitList(final PagedList<T> pagedList) {
if (pagedList != null) {
if (mPagedList == null && mSnapshot == null) {
mIsContiguous = pagedList.isContiguous();
} else {
if (pagedList.isContiguous() != mIsContiguous) {
throw new IllegalArgumentException("AsyncPagedListDiffer cannot handle both"
+ " contiguous and non-contiguous lists.");
}
}
}
//pagedList如果是同一个是不会往下执行的,所以下拉刷新数据是必须要替换掉pagedList
if (pagedList == mPagedList) {
// nothing to do
return;
}
// incrementing generation means any currently-running diffs are discarded when they finish
final int runGeneration = ++mMaxScheduledGeneration;
//传进来的pageList为null,会将之前传进来的pageList置为null,如果不置为null那么新传进来的pageList就会与之前的pageList的数据进行对比,将有变化的item数据进行更新
if (pagedList == null) {
int removedCount = getItemCount();
if (mPagedList != null) {
//清除数据传进来的回调
mPagedList.removeWeakCallback(mPagedListCallback);
mPagedList = null;
} else if (mSnapshot != null) {
mSnapshot = null;
}
// dispatch update callback after updating mPagedList/mSnapshot
mUpdateCallback.onRemoved(0, removedCount);
if (mListener != null) {
mListener.onCurrentListChanged(null);
}
return;
}
//首次添加进来的时候会执行到这里,
if (mPagedList == null && mSnapshot == null) {
// fast simple first insert
mPagedList = pagedList;
//这里就是重点了,请求到的数据和界面刷新就是通过添加的这个回调来关联起来的
pagedList.addWeakCallback(null, mPagedListCallback);
// dispatch update callback after updating mPagedList/mSnapshot
mUpdateCallback.onInserted(0, pagedList.size());
if (mListener != null) {
mListener.onCurrentListChanged(pagedList);
}
return;
}
//传进来pageList时,已经有一个已经存在的pageList了,这时就会将之前的pageList拷贝一份出来,同时将回调清空掉
if (mPagedList != null) {
// first update scheduled on this list, so capture mPages as a snapshot, removing
// callbacks so we don't have resolve updates against a moving target
mPagedList.removeWeakCallback(mPagedListCallback);
mSnapshot = (PagedList<T>) mPagedList.snapshot();
mPagedList = null;
}
if (mSnapshot == null || mPagedList != null) {
throw new IllegalStateException("must be in snapshot state to diff");
}
final PagedList<T> oldSnapshot = mSnapshot;
final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
//有两个pageList时,会执行到这里,将对比的任务放到子线程去执行
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result;
result = PagedStorageDiffHelper.computeDiff(
oldSnapshot.mStorage,
newSnapshot.mStorage,
mConfig.getDiffCallback());
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchPagedList(pagedList, newSnapshot, result);
}
}
});
}
});
}
上面有个方法需要注意,pagedList.addWeakCallback(),在这里添加了一格回调,这个回调就是前面说到的,当数据请求完成时,就会调用到这个回调,现在就来看下在这个回调里面具体做了什么:
private PagedList.Callback mPagedListCallback = new PagedList.Callback() {
@Override
public void onInserted(int position, int count) {
mUpdateCallback.onInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mUpdateCallback.onRemoved(position, count);
}
@Override
public void onChanged(int position, int count) {
// NOTE: pass a null payload to convey null -> item
mUpdateCallback.onChanged(position, count, null);
}
};
使用的也是代理,具体的操作交给了AdapterListUpdateCallback去执行,来看看这里面做了些什么东西:
public final class AdapterListUpdateCallback implements ListUpdateCallback {
@NonNull
private final Adapter mAdapter;
public AdapterListUpdateCallback(@NonNull Adapter adapter) {
this.mAdapter = adapter;
}
public void onInserted(int position, int count) {
this.mAdapter.notifyItemRangeInserted(position, count);
}
public void onRemoved(int position, int count) {
this.mAdapter.notifyItemRangeRemoved(position, count);
}
public void onMoved(int fromPosition, int toPosition) {
this.mAdapter.notifyItemMoved(fromPosition, toPosition);
}
public void onChanged(int position, int count, Object payload) {
this.mAdapter.notifyItemRangeChanged(position, count, payload);
}
}
调用的是RecyclerView.Adapter的方法,也就是去刷新界面。
Q:paging是如何是实现自动加载数据的?
如果我们滑动列表或者其他操作的时候,很自然会调用 adapter 的 bind 方法。那么,我们去查看PagedListAdapter#getItem的源码。
@Nullable
public T getItem(int index) {
if (mPagedList == null) {
if (mSnapshot == null) {
throw new IndexOutOfBoundsException(
"Item count is zero, getItem() call is invalid");
} else {
return mSnapshot.get(index);
}
}
mPagedList.loadAround(index);
return mPagedList.get(index);
}
查看PageList的loadAround,其中调用了抽象方法loadAroundInternal(index);该抽象方法的实现在ContiguousPagedList
protected void loadAroundInternal(int index) {
int prependItems = mConfig.prefetchDistance - (index - mStorage.getLeadingNullCount());
int appendItems = index + mConfig.prefetchDistance
- (mStorage.getLeadingNullCount() + mStorage.getStorageCount());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
if (mPrependItemsRequested > 0) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
}
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
查看scheduleAppend的实现
mBackgroundThreadExecutor.execute(new Runnable() { @Override public void run() { if (isDetached()) { return;
} if (mDataSource.isInvalid()) {
detach();
} else {
mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
}
});
这里,我们看到了dispatchLoadAfter方法的调用,之后的逻辑和之前的dispathLoadInitial就非常的类似了。