谷歌关于 pagign 多数据源示例: https://github.com/googlecodelabs/android-paging
此示例为 kotlin 语言,使用 room + paging 进行翻页请求
当然,为了学习方便,我呕心沥血的把 kotlin 转成 java ,如果对对大家有帮助,请在底部赞赏支持一下
地址:https://github.com/liaozhoubei/Android_Sample
从 android paging 开始学习
android paging 流程:
1.监听 editText ,当发送改变时,使用 LiveData 通知 SearchRepositoriesViewModel 中的 queryLiveData 发生改变
2.queryLiveData 被 repoResult 所监听,repoResult 中的回调方法
// 数据源
DataSource.Factory dataSourceFactory = cache.reposByName(query);
RepoBoundaryCallback boundaryCallback = new RepoBoundaryCallback(query, service, cache);
// 网络请求
MutableLiveData networkErrors = boundaryCallback.getNetworkErrors();
LiveData data = new LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).setBoundaryCallback(boundaryCallback).build();
3.先进行查询数据库的操作,但是查询的结果被包裹在 DataSource.Factory 这个数据源工厂中
4.然后创建 RepoBoundaryCallback 对象,调用了 GithubRepository 的方法进行网络请求。
5.RepoBoundaryCallback是 PagedList.BoundaryCallback 的子类,这是个接口类。它有两个方法:
// PagedList的数据源的初始加载返回零项时调用
onZeroItemsLoaded()
// 当数据源在列表末尾用尽数据时,onItemAtEndLoaded(Object)会调用,
// 并且您可以启动异步网络加载,将结果直接写入数据库。
// 由于正在观察数据库,因此绑定到该UI的UI LiveData将
// 自动更新以考虑新项目。
onItemAtEndLoaded(@NonNull Repo itemAtEnd)
这两个方法相当重要,就是他们构成了 paging下拉刷新的功能
6.RepoBoundaryCallback 接口被设置在 LivePagedListBuilder 里面,从此当数据源在末尾时,就调用 onItemAtEndLoaded() 方法,当没有数据时调用 onZeroItemsLoaded() 方法。
7.在 onItemAtEndLoaded() 中会调用请求网络的方法,请求完网络,就直接插入数据库中
实际上整个流程已经跑完了,然后运行项目发现不断的刷新数据,不断的进行网络请求,最后将请求到的数据插入数据库中。
然而令人迷惑的是网络请求完之后,就只见到数据库数据增多了,但是中间是怎么通过LiveData发送新的数据呢?
奥秘在于 :
LiveData data = new LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).setBoundaryCallback(boundaryCallback).build();
这行代码中,在这里获取了数据库的数据源,从此 Paging 库每次刷新了数据之后,就在内部直接更新了数据,发送到监听的 Observer 之中。
这种操作虽然好,但是在开发的时候令人非常的迷惑,因为数据都在内部更新了,完全不清楚它的走向
paging 中内置的三种 DataSource 的区别
paging 中内置有三种 DataSource ,他们是:
PositionalDataSource
ItemKeyedDataSource
PageKeyedDataSource
这三个都是抽象类,使用这几个类作为模板能够实现一个简单的dataSource。
其中 PositionalDataSource 的父类为 DataSource
- PositionalDataSource 类用于加载在任意位置请求大小的页面,并提供一个固定的项目计数。这个比较容易理解,以数据库为例,我们从头第一条数据开始取,每次需要取固定10条数据,不断往后面去,也就是第一次取出 0-9 条,第二次取 10-19 条,依次类推。
PositionalDataSource 就是起着这个作用,给一个取值范围,会每次都按照取值范围顺序取出固定条数的数据
- ItemKeyedDataSource 用于列表中加载了N条数据,加载下一页数据时,会以列表中最后一条数据的某个字段为Key查询下一页数。它的功能与 PositionalDataSource 类似,都是设置一个固定的条数来取出数据。不同的地方在于 PositionalDataSource 是在请求的时候将当前的请求位置以及请求多少条数据当参数发送,而 ItemKeyedDataSource 则要设置一个 key 来当参数,同时需要后端做支撑。
同样以每次取10条数据为例。
ItemKeyedDataSource 初始请求时,同 PositionalDataSource 一样,获取 0 - 9 条数据。但是下拉更新的时候就不一样了,它需要获取一个 key ,这个 key 是数据中的某条唯一字段,然后将 key 以及 请求的条数 当参数进行请求,获取新的数据。
从后端的角度来说就是获取到一条数据中的唯一字段,然后找到这条数据,查询这条数据中的 位置,最后获取数据当前位置 +1 到后面 10 条数据进行返回
- PageKeyedDataSource 页面中加载了N条数据,每一页数据都会提供下一页数据的关键字Key作为下次查询的依据,基本与 ItemKeyedDataSource 雷同
paging 源码解析
LivePagedListBuilder 初始化
LivePagedListBuilder(new DataSourceFactory(), config)
.setBoundaryCallback(null)
.setFetchExecutor(null)
.build();
最终会生成一个 ComputableLiveData 对象,即可以检测到生命周期的对象,并且实现 compute 抽象方法,用此抽象方法创建 DataSource 和 PageList。如下图:
图片来源:https://blog.csdn.net/Alexwll/article/details/83246201
compute() 方法会在 LiveData 的 onActive() 方法中进行调用,实际是ObserverWrapper的activeStateChanged()方法中调用 onActive()。
我们在仔细看 compute() 方法
protected PagedList compute() {
...
do {
...
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;
}
首先是获取 DataSource 对象,在 LivePagedListBuilder.build 的时候传入了 DataSourceFactory ,在这里回调DataSourceFactory.create(), 创建具体的 DataSource。
其次创建了 PagedList 对象,并且将 DataSource 传入 。PagedList是 paging 中要具操控中的对象。
至此,Paging 完成了第一步,数据源与数据列表的创建与绑定
PagedList 源码
PagedList 继承结构图:
上面在创建 PagedList 的时候使用了 Builder,实质上是调用了 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);
}
}
上面使用 dataSource.isContiguous() 以及 config.enablePlaceholders 判断是创建 ContiguousPagedList 还是 TiledPagedList,它们的意义如下:
dataSource.isContiguous() : 如果数据源保证生成一组连续的项,则返回true,而不会生成空白。
config.enablePlaceholders : 定义PagedList是否可以显示空占位符(如果DataSource提供它们)。
进入 ContiguousPagedList 构造方法 :
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;
}
首先看到这行代码:
super(new PagedStorage(), mainThreadExecutor, backgroundThreadExecutor,
boundaryCallback, config);
这里创建了 PagedStorage 对象,这个对象是保持页面数据的实际对象,实则在里面用 ArrayList 来实现各种增删改查的操作,这个后面再说。
然后调用了 mDataSource.dispatchLoadInitial() 方法,从名字上就可以看出它是处理加载初始化的方法,跟进此方法,发现它是 ContiguousDataSource.dispatchLoadInitial() ,由 ItemKeyedDataSource 以及 PageKeyedDataSource 进行实现。最后调用 loadInitial() 这个由我们具体实现的获取初始化数据的方法,获取数据后,通过接口回调的方式通知主线程,最后通过 LiveData 的 observe()接口返回到需要数据的 Adapter 中。
以上就完成了整个 Paging 的初始化调用。
初始化示例代码如下:
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(10) //配置分页加载的数量
.setEnablePlaceholders(false) //配置是否启动PlaceHolders
.setInitialLoadSizeHint(10) //初始化加载的数量
.build();
LiveData> liveData = new LivePagedListBuilder(
new MyDataSourceFactory(), config).build();
liveData.observe(this,new Observer>() {
@Override
public void onChanged(@Nullable PagedList dataBeans) {
// 每次数据更改后都从此接口获取数据
mAdapter.submitList(dataBeans);
}
});
数据上拉与下拉刷新
我们为什么要用 Paging 这个库,为的就是更便捷的上拉刷新,所以我们必须得清楚它的上拉刷新机制。
我们跟踪一下 PositionalDataSource 上拉时的表现,在上拉的时候,它会调用 loadRange() 方法,所以我们一步步更上去看它的调用链。
最后发现它的调用者为 PagedListAdapter 中的 getItem(int position) 方法,详情如下:
public T getItem(int index) {
...
mPagedList.loadAround(index);
return mPagedList.get(index);
}
这里的 index 实际上是界面上显示的最后一条数据的位置,同时这最后一条数据也是这条数据在已保存的列表中的位置。Paging 会将获取到的数据全部保持到一个 ArrayList 之中。
loadRange() 又调用了 loadAround() 方法,然后 loadAround() 又调用了抽象方法 loadAroundInternal(index),如下
public void loadAround(int index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
}
mLastLoad = index + getPositionOffset();
// 具体加载数据方法
loadAroundInternal(index);
mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
tryDispatchBoundaryCallbacks(true);
}
loadAroundInternal(index) 方法是在 TiledPagedList 与 ContiguousPagedList 中会执行不同逻辑。
TiledPagedList 如下:
@Override
protected void loadAroundInternal(int index) {
mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this);
}
mStorage.allocatePlaceholders 又是什么呢?它是一个 PagedStorage ,用于存储分片数据的类,内部是由一系列 List 的增删改查操作实现的, allocatePlaceholders() 方法如下:
public void allocatePlaceholders(int index, int prefetchDistance,
int pageSize, Callback callback) {
if (pageSize != mPageSize) {
if (pageSize < mPageSize) {
throw new IllegalArgumentException("Page size cannot be reduced");
}
if (mPages.size() != 1 || mTrailingNullCount != 0) {
// not in single, last page allocated case - can't change page size
throw new IllegalArgumentException(
"Page size can change only if last page is only one present");
}
mPageSize = pageSize;
}
final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
allocatePageRange(minimumPage, maximumPage);
int leadingNullPages = mLeadingNullCount / mPageSize;
for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
int localPageIndex = pageIndex - leadingNullPages;
if (mPages.get(localPageIndex) == null) {
//noinspection unchecked
mPages.set(localPageIndex, PLACEHOLDER_LIST);
callback.onPagePlaceholderInserted(pageIndex);
}
}
}
这片代码的逻辑也很简单,只有在 mPages.get(localPageIndex) 也就是下一页分片不存在的时候调用 callback.onPagePlaceholderInserted(pageIndex) ,此方法会在线程中调用 PositionalDataSource 的 loadRange() 方法。另外 onPagePlaceholderInserted() 方法只在 TiledPagedList 中实现, ContiguousPagedList中执行会抛出异常
ContiguousPagedList 如下:
@MainThread
@Override
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) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
}
其中有两个重要的变量
mPrependItemsRequested :向前需要多少条数据,当其 >0 时,会调用 DataSource(ItemKeyedDataSource与PageKeyedDataSource) 的 loadBefore() 方法(PositionalDataSource 则调用 dispatchLoadRange() 方法)
mAppendItemsRequested : 向后需要多少条数据,当其 >0 时,会调用 DataSource 的 loadAfter() 方法
mPrependItemsRequested 的值为获取自己与 prependItems 之间的最大值 ,mAppendItemsRequested 则是获取自己与 appendItems 的最大值。prependItems 与 appendItems 则是通过以下两个方法计算出来的。
static int getPrependItemsRequested(int prefetchDistance, int index, int leadingNulls) {
return prefetchDistance - (index - leadingNulls);
}
static int getAppendItemsRequested(
int prefetchDistance, int index, int itemsBeforeTrailingNulls) {
return index + prefetchDistance + 1 - itemsBeforeTrailingNulls;
}
方法中第一个参数 prefetchDistance ,一般是设置 PagedList.Config 时的 PageSize , 其含义为:预取距离,用于定义加载前的距离。如果此值设置为50,则分页列表将尝试提前加载50个项目已经访问过的数据。
第二个参数 index 则是当前 item 所处的位置,上面有提及。
第三个参数 leadingNulls 和 itemsBeforeTrailingNulls 表示列表中为 null 的数据的个数,可不理会。
现在我们就知道上拉和下拉的逻辑了。假如我们设置每页的数据需要 10 条,当我们把页面往上拖动,需要加载更多数据,这时当前数据存量为10条,此时页面底部的 index 为 10,拖拽时,appendItems = 10(index)+ 10(mConfig.prefetchDistance) - 10(mStorage.getStorageCount() 当前数据存量),最后得到结果为 10,这时就需要请求网络加载更多。
加载完毕后在 onPageAppended() 方法更新 mAppendItemsRequested 的值,如下:
@MainThread
@Override
public void onPageAppended(int endPosition, int changedCount, int addedCount) {
// consider whether to post more work, now that a page is fully appended
mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
mAppendWorkerState = READY_TO_FETCH;
if (mAppendItemsRequested > 0) {
// not done appending, keep going
scheduleAppend();
}
// finally dispatch callbacks, after append may have already been scheduled
notifyChanged(endPosition, changedCount);
notifyInserted(endPosition + changedCount, addedCount);
}
如此便完成了一个闭环。
同理,下拉刷新时也是同样进行更新数据,当界面中最顶上的 index 小于预设的 PageSize 的时候,就会触发 loadBefore() ,最后在 PagedStroage 将加载出来的数据加到 ArrayList 的最顶上。
PagedList.Config 配置信息
PagedList.Config 的配置参数会影响到 Paging 的 DataSource 的使用,以下稍微解释一下这些参数:
setEnablePlaceholders(boolean) : 此选项会影响到具体初始化那种 PageList,而不同的 PageList 会影响到是否要调用 BoundaryCallback 这个监听数据是否到达边界的回调。
当设置为 true 的时候,会将数据源初始化为 PositionalDataSource ,为fasle 时初始化为:ContiguousDataSource 。也就是说实例化 DataSource 的时候并不一定取决于我们设置了哪种 DataSource ,而是取决于 PagedList.Config 的参数。 PagedList 初始化 DataSource 代码如下(PagedList.create()):
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) {
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);
}
从中可看出仅有 dataSource.isContiguous() 为 ture 以及 config.enablePlaceholders 为 false 时才会初始化 ContiguousPagedList ,其余的时候都初始化 TiledPagedList。
其次,config.enablePlaceholders 的默认值为 ture, 也就是说默认情况下初始化的都是 TiledPagedList 。
在实际测试中,config.enablePlaceholders 为 false, BoundaryCallback 为 null 时, 并且 DataSource 为 PositionDataSource 的时候,此时界面中可正常加载数据。
但当 config.enablePlaceholders 为 true 时,却发现界面只加载了第一份数据,不在继续往下加载。
究其原因:
PositionalDataSource 在上拉的时候,是调用子类的 landRange 方法,
当 config.enablePlaceholders 为 true 时,会初始化PositionalDataSource,同时 PageList 初始化为 TiledPagedList。在 TiledPagedList 的 loadAroundInternal 上拉刷新中,此时会调用 PagedStorag.allocatePlaceholders 方法,在这个方法中判断是否要调用 PositionalDataSource.landRange,然而 mPages.get(localPageIndex) 不会为 null, 导致landRange不被调用。如下(位于 PagedStorag.allocatePlaceholders() ):
public void allocatePlaceholders(int index, int prefetchDistance,
int pageSize, Callback callback) {
....
for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
int localPageIndex = pageIndex - leadingNullPages;
if (mPages.get(localPageIndex) == null) {
Log.e("PagedStorage", "mPages.get(localPageIndex) == null" );
//noinspection unchecked
mPages.set(localPageIndex, PLACEHOLDER_LIST);
callback.onPagePlaceholderInserted(pageIndex);
}
}
}
ContiguousDataSource 在上拉的时候是调用 loadAfter 方法。
当 config.enablePlaceholders 为 false 时,会初始化 ContiguousWithoutPlaceholdersWrapper, 将 会初始化PositionalDataSource 包装为 ContiguousDataSource, 同时 PageList 初始化为 ContiguousPagedList。在ContiguousPagedList 的上拉刷新 loadAroundInternal 中调用scheduleAppend, 然后调用 ContiguousDataSource。dispatchLoadAfter,接着调用 ContiguousWithoutPlaceholdersWrapper.dispatchLoadAfter,最后调用到 PositionalDataSource.landRange 刷新数据。
dataSource.isContiguous() 是 dataSource 的抽象方法,由子类赋值, PositionDataSource / TitledDataSource 为 false, ContiguousDataSource 为 true . 同时 PagedList 中也有此参数,但是与 DataSource 中保持一致。
BoundaryCallback
// 如果设置为true,则mBoundaryCallback为非null,并且应在附近加载时调度
private boolean mBoundaryCallbackBeginDeferred = false;
private boolean mBoundaryCallbackEndDeferred = false;
// loadAround访问的最低和最高索引。 用于决定何时应调度mBoundaryCallback
private int mLowestIndexAccessed = Integer.MAX_VALUE;
private int mHighestIndexAccessed = Integer.MIN_VALUE;
// 此方法在 DataSource 的 loadInitial() / loadRange() 调用后,调用 onResult(@NonNull List data) 时调用
deferBoundaryCallbacks(final boolean deferEmpty,
final boolean deferBegin, final boolean deferEnd)
// mLowestIndexAccessed / mHighestIndexAccessed已更新,因此请检查是否需要调度边界回调。 边界回调延迟到最后一项加载,并且访问发生在边界附近。
// 注意:我们在此处发布,因为RecyclerView可能希望在响应中添加项目,并且此调用发生在PagedListAdapter bind中。
tryDispatchBoundaryCallbacks(true);
关于 BoundaryCallback 的理解还不够透彻,目前有个疑惑就是在单数据源的时候,上拉到底部不会调用 BoundaryCallback.onItemAtEndLoaded() 方法,而是调用 DataSource 的加载更多的方法。目前还没有研究透彻,先搁置一边吧。