转载请标明出处https://www.jianshu.com/p/b9c7a6e3e8d2
前言:最近有个需求,要考虑到系统资源以及网络请求的效率,需要做一个类似于现在市面上那种列表页面可以往上滑动不断加载item的效果,想着自己写逻辑,对控件recycleview的滑动到底部的事件进行判定之后请求数据,再去对adapter进行数据的判断增加然后视图刷新。一箩筐下来觉得好麻烦啊,就去Google了一下,发现官方提供了Paging库来处理这个场景。由于这个框架也是运用到LiveData,不熟悉的朋友可以先看一下之前LiveData的文章。
1.Paging
Paging是一个官方提供的分页库。使用这个库,我们只需要关心数据,分页和视图是不需要我们去关心的,这个库会帮我们实现。在这个库,最重要的就是关键组件是PagedList类。而且一般来说,这个库都会搭配着RXjava2或者LiveData来使用,友好的处理控件和数据的生命周期。本文的例子将会使用LiveData。
2.Paging的配合
2.1.数据
列表数据的来源,可分为本地数据和网络数据,本地数据官方是建议使用Room持久库来整理数据的,而网络数据,可以使用自己定义的数据源工厂,本文例子也将使用自定义数据源工厂。
22.界面
分页展示,这个库需要搭配recyclerview来进行展示,recyclerview也会有Paging提供的特殊adapter类。
3.使用
二话不多说,项目依赖走起。
implementation "androidx.paging:paging-runtime:2.1.2"
testImplementation "androidx.paging:paging-common:2.1.2"
implementation "androidx.paging:paging-rxjava2:2.1.2"
分页列表的实现,界面由recyclerview来实现,这里recyclerview要注意,要继承Paging库提供的PagedListAdapter类,这是实现分页效果的关键。
public class AppleAdapter extends PagedListAdapter {
使用这个类我们不需要再重写getItemCount方法,PagedListAdapter自己重写了getItemCount,我们只需要通过设置DiffUtil来使得它可以对数据差异进行判断。可以通过item特有属性的对别或者item的整个对象的对比来得出差异,从而决定是否要更新到列表中去。
public AppleAdapter() {
super(DIFF_CALLBACK);
}
public static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull apple oldApple, @NonNull apple newApple) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldApple.getId() == newApple.getId();
}
@Override
public boolean areContentsTheSame(@NonNull apple oldApple, @NonNull apple newApple) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return newApple.equals(newApple);
}
};
其次就是数据了,这里我自己定义了数据源工厂类。配合了LiveData进行使用。先来看一下数据源工厂类。
private class MyAppleSourceFactory extends DataSource.Factory {
private MutableLiveData sourceMutableLiveData = new MutableLiveData<>();
private MyAppleSource source;
@NonNull
@Override
public DataSource create() {
source = new MyAppleSource();
//查看Google的文档也没看明白这个liveData是为什么
//但是猜测可能是想利用liveData对-生命周期进行监听,有懂的朋友可以评论不吝赐教。
sourceMutableLiveData.postValue(source);
return source;
}
}
private class MyAppleSource extends ItemKeyedDataSource {
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) {
List items = getMoreMyApple(0);
callback.onResult(items);
}
@Override
public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) {
List items = getMoreMyApple((Integer) params.key);
callback.onResult(items);
}
@Override
public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) {
}
@NonNull
@Override
public Integer getKey(@NonNull Apple item) {
return item.getId();
}
}
工厂类内部实例化了一个Source类,这个类我这里是继承了ItemKeyedDataSource,他是规定整个分页是由item的某一个属性去获取数据的。官方还提供了其余2个Source类:PageKeyedDataSource,PositionalDataSource。解决各自的特定场景,需要的朋友可以自己Google了解一下。
然后看一下控件和工厂类数据的初始化,我这里是用了Viewmodel来实现demo的。
public LiveData> appleMutableLiveData;
public void initMyApple() {
MyAppleSourceFactory appleSourceFactory = new MyAppleSourceFactory();
myAppleSource = appleSourceFactory.create();
//pageList的LiveData由activity这个UI层去进行监听。
appleMutableLiveData = new LivePagedListBuilder(appleSourceFactory, 10).build();
}
最后看一下Activity,
appleMyBinding = DataBindingUtil.setContentView(this, R.layout.activity_apple_my);
//苹果列表初始化,其实就是正常的recyclerview的管理器布局器设置
initRVApple();
myAppleViewModel = ViewModelProviders.of(this).get(AppleViewModel.class);
//PagedList的LiveData初始化
myAppleViewModel.initMyApple();
myAppleViewModel.appleMutableLiveData.observe(this, appleVOS -> {
//停止加载,通知回调
myAppleViewModel.invalidateDataSource();
//游戏列表数据变化监听,其实这个操作相当于向adapter传递数据的过程。
//这也是一个LiveData连接PagedListAdapter的过程
//PagedListAdapter会自动处理分页差异更新
appleAdapter.submitList(appleVOS);
});
这里关键其实是appleAdapter.submitList(appleVOS)。是由于这个方法,recyclerview和数据搭上了线。
到这里代码就写的差不多了。activity中recyclerview的初始化,数据源工厂类的自定义,viewmodel中的数据源工厂类初始化。通过这一系列操作就可以实现分页的效果。
在不知道他的实现原理的情况下,我们大胆猜测一下,整个流程是这样的,在ViewModel中LiveData通常是去通过model层获取数据设置数据到LiveData对象中的,也就是当分页开始请求数据的时候,数据来源于工厂类,它实际上会走Source类的loadInitial方法设置数据,之后界面每次数据不够显示了,就会调用loadAfter的方法去设置数据。实际中getMoreMyApple也是对应了Model层,是一个关于数据的网络请求方法,根据数据的id去请求数据。数据设置到LiveData中了,通过Adapter.submitList绑定到界面。那么关键就在于,装有PagedList的LiveData是如何得到的。咱们来看一下源码。
4.原理简析
官方写法是,在activity新建时viewmodel实例化,一起调用LivePagedListBuilder而获得一个LiveData对象,这个对象中包含了PagedList对象。这个LiveData是如何获得,咱们一层一层剥开看看。
appleMutableLiveData = new LivePagedListBuilder(appleSourceFactory, 10).build();
这个build到底做了什么可以得到一个LiveData,点开源码瞧瞧。
public LiveData> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}
他返回的是一个create方法执行得到的结果。看看create方法。
private static LiveData> create(
@Nullable final Key initialLoadKey,
@NonNull final PagedList.Config config,
@Nullable final PagedList.BoundaryCallback boundaryCallback,
@NonNull final DataSource.Factory dataSourceFactory,
@NonNull final Executor notifyExecutor,
@NonNull final Executor fetchExecutor) {
return new ComputableLiveData>(fetchExecutor) {
@Nullable
private PagedList mList;
@Nullable
private DataSource mDataSource;
private final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};
@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList 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();
}
create方法返回的则是一个ComputableLiveData类的实例化对象的getLiveData的值,看看这个ComputableLiveData类构造方法以及这个getLiveData方法。
public ComputableLiveData(@NonNull Executor executor) {
mExecutor = executor;
mLiveData = new LiveData() {
@Override
protected void onActive() {
mExecutor.execute(mRefreshRunnable);
}
};
}
@NonNull
public LiveData getLiveData() {
return mLiveData;
}
原来这一系列下来是一个LiveData对象实例化以及返回的过程。而且当这个liveData启用的时候会在线程池中运行一个子线程。咱们看看这个子线程了干了些什么。
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);
}
}
// check invalid after releasing compute lock to avoid the following scenario.
// Thread A runs compute()
// Thread A checks invalid, it is false
// Main thread sets invalid to true
// Thread B runs, fails to acquire compute lock and skips
// Thread A releases compute lock
// We've left invalid in set state. The check below recovers.
} while (computed && mInvalid.get());
}
};
这个子线程中对LiveData进行了postValue,这下子就清楚,这就是build方法返回的装有PagedList的liveData数据变化,UI也会变化了。而这个postValue的值来源于compute抽象方法的,咱们回头看看在LivePagedListBuilder中实例化的ComputableLiveData对象的compute方法具体是怎么实现的。
@Override
protected PagedList 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;
}
他post的value原来是来自于这个方法新建的PagedList对象,而他的值就是来源于dataSourceFactory的mDataSource对象的。这个factory对象就是我们build的时候传入的自定义数据源工厂类了。然后再来看看这个PagedList是怎么build生成一个PagedList对象的。
public PagedList build() {
// TODO: define defaults, once they can be used in module without android dependency
if (mNotifyExecutor == null) {
throw new IllegalArgumentException("MainThreadExecutor required");
}
if (mFetchExecutor == null) {
throw new IllegalArgumentException("BackgroundThreadExecutor required");
}
//noinspection unchecked
return PagedList.create(
mDataSource,
mNotifyExecutor,
mFetchExecutor,
mBoundaryCallback,
mConfig,
mInitialKey);
}
}
由PagedList类create方法实例化的,往下看。
static PagedList create(@NonNull DataSource dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback boundaryCallback,
@NonNull Config config,
@Nullable K key) {
if (dataSource.isContiguous() || !config.enablePlaceholders) {
int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
if (!dataSource.isContiguous()) {
//noinspection unchecked
dataSource = (DataSource) ((PositionalDataSource) dataSource)
.wrapAsContiguousWithoutPlaceholders();
if (key != null) {
lastLoad = (Integer) key;
}
}
ContiguousDataSource contigDataSource = (ContiguousDataSource) dataSource;
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
} else {
return new TiledPagedList<>((PositionalDataSource) dataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
(key != null) ? (Integer) key : 0);
}
}
看着源码,这个source对象通过一系列判断最后会转为两种PagedList类,不过ContiguousDataSource比较特别,我们点进去看看。
ContiguousDataSource wrapAsContiguousWithoutPlaceholders() {
return new ContiguousWithoutPlaceholdersWrapper<>(this);
}
ContiguousWithoutPlaceholdersWrapper(
@NonNull PositionalDataSource source) {
mSource = source;
}
ContiguousPagedList(
@NonNull ContiguousDataSource dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback boundaryCallback,
@NonNull Config config,
final @Nullable K key,
int lastLoad) {
super(new PagedStorage(), mainThreadExecutor, backgroundThreadExecutor,
boundaryCallback, config);
mDataSource = dataSource;
mLastLoad = lastLoad;
if (mDataSource.isInvalid()) {
detach();
} else {
mDataSource.dispatchLoadInitial(key,
mConfig.initialLoadSizeHint,
mConfig.pageSize,
mConfig.enablePlaceholders,
mMainThreadExecutor,
mReceiver);
}
mShouldTrim = mDataSource.supportsPageDropping()
&& mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
}
TiledPagedList(@NonNull PositionalDataSource dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback boundaryCallback,
@NonNull Config config,
int position) {
super(new PagedStorage(), mainThreadExecutor, backgroundThreadExecutor,
boundaryCallback, config);
mDataSource = dataSource;
final int pageSize = mConfig.pageSize;
mLastLoad = position;
if (mDataSource.isInvalid()) {
detach();
} else {
final int firstLoadSize =
(Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize;
final int idealStart = position - firstLoadSize / 2;
final int roundedPageStart = Math.max(0, idealStart / pageSize * pageSize);
mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
pageSize, mMainThreadExecutor, mReceiver);
}
}
这两个PagedList方法逻辑相似一上来就会对DataSource再判断,然后决定是否走PositionalDataSource的dispatchLoadInitial方法。
这个方法看着好眼熟,会让人想起自定义的source类不是吗。咱们往下看。
final void dispatchLoadInitial(boolean acceptCount,
int requestedStartPosition, int requestedLoadSize, int pageSize,
@NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) {
LoadInitialCallbackImpl callback =
new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
LoadInitialParams params = new LoadInitialParams(
requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
loadInitial(params, 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);
}
这个方法,直接就调用我们自定义source类的loadInitial方法了。就可以拿到自定义请求方法得到的PagedList了,还有loadAfter和loadBefore的调用过程这里就不再深入了。
接下来来看一下,这个source中的关于数据加载时机的回调,是怎么配合PagedListAdapter的。看看Adapter。
public AppleAdapter() {
super(DIFF_CALLBACK);
}
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.addPagedListListener(mListener);
}
private final AsyncPagedListDiffer.PagedListListener mListener =
new AsyncPagedListDiffer.PagedListListener() {
@Override
public void onCurrentListChanged(
@Nullable PagedList previousList, @Nullable PagedList currentList) {
PagedListAdapter.this.onCurrentListChanged(currentList);
PagedListAdapter.this.onCurrentListChanged(previousList, currentList);
}
};
这里先记住,它内部会实例化一个AsyncPagedListDiffer对象并且赋予回调以及设置PagedListListener。
然后我们来看一下PagedListAdapter的submit方法。因为是依赖这个方法实现数据和界面的绑定的。
appleAdapter.submitList(apples);
public void submitList(@Nullable PagedList pagedList) {
mDiffer.submitList(pagedList);
}
发现是调用了AsyncPagedListDiffer这个类的submitList方法。往里面深处翻发现
public void submitList(@Nullable final PagedList pagedList,
@Nullable final Runnable commitCallback) {
这里有个关键方法,onCurrentListChanged(previous, null, commitCallback);
private void onCurrentListChanged(
@Nullable PagedList previousList,
@Nullable PagedList currentList,
@Nullable Runnable commitCallback) {
for (PagedListListener listener : mListeners) {
listener.onCurrentListChanged(previousList, currentList);
}
if (commitCallback != null) {
commitCallback.run();
}
}
实际上他就是调用了上面PagedListListener的onCurrentListChanged,走的是PagedListAdapter的onCurrentListChanged方法的。看了一下源码注释和翻了一下文档,这是一个当前PagedList更新时调用的两个方法。
到这里就差不多知道整个流程了。通过LivePagedListBuilder拿到工厂类的Source,通过一系列回调走到我们自定义loadInitial方法,由此拿到PageList对象,将一个PagedList对象传递给Adapter,当页面变化,调用AsyncPagedListDiffer的onCurrentListChanged,就会触发Adapter的onCurrentListChanged。
5.总结
刚开始用的时候,由于都不知道其原理,只能先一边看文档一边写个demo,功能实现了,就是搞不明白为什么要写factory为什么要写source,为什么是LivePagedListBuilder去build拿到LiveData的,为什么通过submitList就可以让数据绑定到控件,然后随着控件滑动就可以触发source的loadAfter了。通过翻看源码结合注释以及翻看文档。终于懂得一二,总算知道了为什么可以通过这样一个流程实现一个分页的效果了。