看一下官方的paging动态图,数据源里面插入一条数据的
其核心就三部分:DataSource(数据源)、PagedList(分页组件)、PagedListAdapter(适配器)
当数据源发生改变,自动通知Adapter改变,从而完成界面上的展示变化。使用观察者模式来完成,数据源就是被观察者对象,而Adapter则是观察者。
paging库源码的两部分
common是其抽象出来的代码,而runtime则是其实现的代码
看其抽象的类图可以看到DataSource有众多的实现,但是其划分也就两种,单点源和连续源。
单点源(PositionalDataSource):是指一次获取出来所有的数据,当数据源里面数据有增删改时,观察者自动更新。
连续源(ContiguousDataSource):连续源就是我们的分页加载,当下滑到一定的位置时,就自动获取新的一页数据。
官方demo,是单点源方式。相对要简单一些,添加一条数据,通过viewModel将数据保存到数据库,viewModel已改变,其观察者响应变化的
//添加数据后的新数据 it
viewModel.allCheeses.observe(this, Observer(){
adapter.submitList(it)
})
上面代码是数据adapter改变,其内部通过子线程做数据对比,对比完成后,切换到主线程更新界面,对比完成后的,AsyncPagedListDiffer.latchPagedList方法完成界面的刷新,关键代码并见下
// dispatch update callback after updating mPagedList/mSnapshot
PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
previousSnapshot.mStorage, newList.mStorage, diffResult);
dispatchDiff方法完成数据的回调,具体是插入,删除,修改,在这些判断 ,并完成回调。
/**
* TODO: improve diffing logic
*
* This function currently does a naive diff, assuming null does not become an item, and vice
* versa (so it won't dispatch onChange events for these). It's similar to passing a list with
* leading/trailing nulls in the beginning / end to DiffUtil, but dispatches the remove/insert
* for changed nulls at the beginning / end of the list.
*
* Note: if lists mutate between diffing the snapshot and dispatching the diff here, then we
* handle this by passing the snapshot to the callback, and dispatching those changes
* immediately after dispatching this diff.
*/
static <T> void dispatchDiff(ListUpdateCallback callback,
final PagedStorage<T> oldList,
final PagedStorage<T> newList,
final DiffUtil.DiffResult diffResult) {
final int trailingOld = oldList.computeTrailingNulls();
final int trailingNew = newList.computeTrailingNulls();
final int leadingOld = oldList.computeLeadingNulls();
final int leadingNew = newList.computeLeadingNulls();
if (trailingOld == 0
&& trailingNew == 0
&& leadingOld == 0
&& leadingNew == 0) {
// Simple case, dispatch & return
diffResult.dispatchUpdatesTo(callback);
return;
}
// First, remove or insert trailing nulls
if (trailingOld > trailingNew) {
int count = trailingOld - trailingNew;
callback.onRemoved(oldList.size() - count, count);
} else if (trailingOld < trailingNew) {
callback.onInserted(oldList.size(), trailingNew - trailingOld);
}
// Second, remove or insert leading nulls
if (leadingOld > leadingNew) {
callback.onRemoved(0, leadingOld - leadingNew);
} else if (leadingOld < leadingNew) {
callback.onInserted(0, leadingNew - leadingOld);
}
// apply the diff, with an offset if needed
if (leadingNew != 0) {
diffResult.dispatchUpdatesTo(new OffsettingListUpdateCallback(leadingNew, callback));
} else {
diffResult.dispatchUpdatesTo(callback);
}
}
连续源
在实际场景中,我们通常不会使用单点源,而是使用支持分页网络加载的连续源方式。而paging的连续源分页用户从体验上来说,几乎感觉不到有分页的,因为它在移动过程中并不是到底部才开始加载数据,而是在移动快到底部时就加载,这样给用户的感觉是将所有数据都加载出来了,提升了用户体验。
而使用连续源,DataSource根据需要使用PageKeyedDataSource或ItemKeyedDataSource。
其区别在于, 个人理解
PageKeyedDataSource:按页加载 1,2 ,3
ItemKeyedDataSource: 按索引加载,忽略前多少个
ViewModel数据更新后,adapter同样是使用submitList()方法,来刷新界面。 与单点源不同的是,内部的PagedList使用不同,单点源使用TiledPagedList,而连续源使用的ContiguousPgedList。
pagedList控制分布加载的关键点。
这里提几个问题:
- Q1:初始化时,第一次请求怎么调用的?
- Q2:快滑动到底部时是怎么自动加载数据的?
Q1:初始化时,第一次请求怎么调用的?
ViewModel 内部的列表一般会使用LiveData类型,而对于可变的列表,需要使用MutableLiveData。
其postValue()方法就是对DataSource源的设置。postValue()是一个异步方法,它还有个同步的方法setValue()。代码见下:
protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}
可以看到它post了一个mPostValueRunnable ,在里面完成数据mData的设置。
private final Runnable mPostValueRunnable = new Runnable() {
@Override
public void run() {
Object newValue;
synchronized (mDataLock) {
newValue = mPendingData;
mPendingData = NOT_SET;
}
//noinspection unchecked
setValue((T) newValue);
}
};
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
在paging里面提供了一个LivePagedListBuilder构建类,它作用是获得分布加载更多的LiveData实例。其内部使用可计算的LiveData(ComputableLiveData),它作为被观察者对象,其观察者响应结果mAdapter.submitList(it),从而完界面的刷新。
ComputableLiveData内部的compute()方法实现,
mList:它数据通过PagedList的建造者模式实例化出来。
mDataSource:工厂模式创建出真正的DataSource对象。
除了这两个重要的成员外,还有两个Runnable对象:mRefreshRunnable;分别负责刷新的回调,包括了下拉刷新,加载更多。只是下拉刷新的情况,它使用mInvalidationRunnable,其内部再调用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) {
mLiveData.postValue(value);
}
} finally {
// release compute lock
mComputing.set(false);
}
}
} while (computed && mInvalid.get());
}
};
// invalidation check always happens on the main thread
@VisibleForTesting
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = mLiveData.hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
mExecutor.execute(mRefreshRunnable);
}
}
}
};
我们通常的下拉刷新,通过ComputableLiveData.invalidate()可重新获取数据,因为其mInvalid成员变量,当它设置为true,会调用compute(),这个方法是虚方法,看其具体实现,就是在LivePagedListBuilder构建的时候重写的,其后的mRefreshRunnable动作才会postValue()将新的值发送出去,观察者就立即响应。
public void invalidate() {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
这就是下拉刷新的过程,ViewModel的执行过程,当然,界面上的刷新改变则是观察者响应的后续执行过程。
Q2:快滑动到底部时是怎么自动加载数据的?
自动执行了加载更多,关键点在哪?
答案就是:AsyncPagedListDiffer.getItem(),getItem()内部有一个关键的方法,见下
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);
}
loadAround -> loadAroundInternal,由于loadAroundInternal同样是虚方法,其实现到ContiguousPagedList类,其中的关键方法 schedulePrepend()、scheduleAppend(); 分别是加载已有但是未在界面上展示的数据,加载网络数据,避免了每调用一次getItem,就触发一次请求。
protected void loadAroundInternal(int index) {
...
if (mPrependItemsRequested > 0) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
}
加载更多,即scheduleAppend()。
@MainThread
private void scheduleAppend() {
if (mAppendWorkerRunning) {
return;
}
mAppendWorkerRunning = true;
final int position = mStorage.getLeadingNullCount()
+ mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
// safe to access first item here - mStorage can't be empty if we're appending
final V item = mStorage.getLastLoadedItem();
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (isDetached()) {
return;
}
if (mDataSource.isInvalid()) {
detach();
} else {
mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
}
});
}
可见,这里就回到了 mDataSource.dispatchLoadAfter,若使用的PageKeyedDataSource,那就是它是dispatchLoadAfter方法,最终调用到loadAfter(),在其内部实现加载更多的网络请求,从而实现了加载更多。
还有一个疑惑,什么时候可以加载?
看看mAppendWorkerRunning在哪里设置为false的,可以看到在onPageAppended(),它又是由appendPage()里面的callBack回调来的,appendPage()在PageStorage.Receiver对象onPageResult()方法调用的,即mReceiver。
再看mReceiver被谁使用了?PageKeyedDataSource.LoadInitialCallbackImpl、 PageKeyedDataSource.LoadCallbackImpl,这两个类内部都是LoadCallbackHelper对象,被dispatchResultToReceiver()调用,而dispatchResultToReceiver被LoadInitialCallbackImpl和LoadCallbackImpl的onResult()回调。
LoadCallBackImpl的onResult()则是网络请求成功后,调用的。
也就是说,在网络请求成功后,mAppendWorkerRunning就置为false。
loadAroundInternal() 里面有判断,下一次请求必须 mAppendItemsRequested>0,即 index+可展示利用数+1-实际item总数 >0,才进行网络加载。