Android组件之Paging的使用及原理
流程
● 创建DataSource
● 创建Factory
● 创建Adapter相关类
● 创建LivePagedListBuilder
● 监听LiveData
类
DataSource
处理数据源相关抽象类,DataSource<Key, Value>Key是用来帮助开发者进行数据的组合以及请求的变化,会在请求开始和过程中传递给开发者,Key的类型和值由开发者决定
Value就是数据源的实体类
有三个默认提供的类:
● PageKeyedDataSource<Key, Value>:
如果页面在加载时插入一个/下一个键,例如:从网络获取社交媒体的帖子,可能需要将nextPage加载到后续的加载中
● ItemKeyedDataSource<Key, Value>:
在需要让使用的数据的item从N条增加到N+1条时使用,一般的请求用这个类可以大部分解决,KEY值传page页数即可
● PositionalDataSource<T>:
如果需要从数据存储的任意位置来获取数据页面。此类支持你从任何位置开始请求一组item的数据集。例如,该请求可能会返回从位置1200条开始的20个数据项,适合用于本地数据源的加载
PageList
从DataSource获取不可变数量的数据,可以通过Config进行各种配置,将数据提交给Adapter进行展示
PageList.Config
对数据如何处理的配置,控制加载多少数据,什么时候加载
PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(20)
.setPageSize(1).build()
● int pageSize:每个页面需要加载多少数量
● int prefetchDistance:滑动剩余多少item时,去加载下一页的数据,默认是pageSize大小
● boolean enablePlaceholders:开启占位符
● int initialLoadSizeHint:第一次加载多少数据量
Placeholders
占位列表,在你的列表未加载前,是否显示占位列表,就像各类新闻app中,没加载之前,列表显示的都是一些默认的灰色的布局,默认开启
优点:
● 支持滚动条
● 不需要loading提示,因为List大小是确定的
缺点:
● 必须确定List的大小
● 需要每个item大小一致
● 需要Adapter触发未加载数据的加载
PagedListAdapter(DiffUtil.ItemCallback<T> diffCallback)
RecyclerView.Adapter的一个实现类,用于当数据加载完毕时,通知 RecyclerView数据已经到达。 RecyclerView就可以把数据填充进来,取代原来的占位元素。
数据变化时,PageListAdapter会接受到通知,交由委托类AsyncPagedListDiffer来处理,AsyncPagedListDiffer是对DiffUtil.ItemCallback<T>持有对象的委托类,AsyncPagedListDiffer使用后台线程来计算PagedList的改变,item是否改变,由DiffUtil.ItemCallback<T>决定
DataSource.Factory<Key, Value>
如何创建DataSource的Factory类,主要工作是创建DataSource
LivePagedListBuilder
根据提供的Factory和PageConfig来创建数据源,返回数据为LiveData<PagedList<Value>>
val pagedList: LiveData<PagedList<Entity>> = LivePagedListBuilder(sourceFactory, pagedListConfig).build()
BoundaryCallback
经常用于与ROOM结合使用时
public abstract static class BoundaryCallback<T> {
//如果本地数据获取到的数量为 0 时调用
public void onZeroItemsLoaded() {}
//一般不做处理
public void onItemAtFrontLoaded(@NonNull T itemAtFront) {}
// 最后一个item加载时调用
// 假设ROOM中存了1000条数据,但并不是这1000全部拿出来,而是根据你设置的pagedListConfig来加载数据
//当滑动的时候 根据pagedListConfig的规则,最后一个item需要被加载时,才会调用该方法
public void onItemAtEndLoaded(@NonNull T itemAtEnd) {}
}
例子
DataSource:
class ListDataSource : ItemKeyedDataSource<Int,Entity>() {
var page: Int = 1
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Entity>) {
//初始请求数据,必须要同步请求
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Entity>) {
//请求后续数据,异步
page++
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<V>) {
}
override fun getKey(item: Entity): Int {
return page
}
Factory:
class ListFactory : DataSource.Factory<Int, Entity>() {
var sourceLiveData = MutableLiveData<ListDataSource>()
override fun create(): DataSource<Int, Entity> {
val dataSource = ListDataSource()
sourceLiveData.postValue(dataSource)
return dataSource
}
}
Adapter:
class ListAdapter() :
PagedListAdapter<Entity, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<Entity>() {
override fun areItemsTheSame(oldItem: Entity, newItem: Entity): Boolean =
oldItem.name == newItem.name
override fun areContentsTheSame(oldItem: Entity, newItem: Entity): Boolean =
oldItem == newItem
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = ListViewHolder.create(parent)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ListViewHolder).bind(getItem(position))
}
}
ViewHolder:
class ListViewHolder(parent: View) : RecyclerView.ViewHolder(parent) {
private val contentTv = itemView.findViewById<TextView>(R.id.contentTv)
fun bind(entity: WeiboMessageEntity?) {
contentTv.name = entity?.name
}
companion object {
fun create(parent: ViewGroup): ListViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item, parent, false)
return ListViewHolder(view)
}
}
}
LivePagedListBuilder:
val sourceFactory = ListFactory()
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(20*2)
.setPageSize(20)
.build()
val pagedList: LiveData<PagedList<Entity>> = LivePagedListBuilder(sourceFactory, pagedListConfig).build()
UI:
viewModel.pagedList.observe(this, Observer {
listAdapter?.submitList(it)
})
原理
从使用角度分析,从LivePagedListBuilder开始
- LivePagedListBuilder-build(根据Factory和DataSource来构建包含数据源LiveData的PageList)
- 创建 ComputableLiveData(创建的时候就会执行mRefreshRunnable),创建后返回LiveData
private static <Key, Value> LiveData<PagedList<Value>> create(...) {
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
...
@Override
protected PagedList<Value> compute() {
...
return mList;
}
}.getLiveData();
- 执行Runnable,mRefreshRunnable线程完成计算工作后,调用mLiveData.postValue(value), View层的Observer则会接收到结果
public ComputableLiveData(@NonNull Executor executor) {
mExecutor = executor;
mLiveData = new LiveData<T>() {
@Override
protected void onActive() {
mExecutor.execute(mRefreshRunnable);
}
};
}
@VisibleForTesting
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());
}
};
- computed() 计算工作主要是创建PageList,只会计算一次,计算完后会返回List
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;
}
- 一系列的创建以及调用工作
● PageList-build-create
● 创建 ContiguousPagedList 或 TiledPagedList(if isContiguous is true) 如果保证数据的item数不会变化,则可以设置这个属性
● 调用 dispatchLoadInitial
● 创建 LoadInitialCallbackImpl
● 调用我们需要编写代码的 loadInitial(如果此时加载数据失败可以调用loadInitial()重新进行请求)
● 调用 callBack.onResult() 返回数据
● 回调至 LoadInitialCallbackImpl
- 根据原数据重新创建PageList,调用dispatchResultToReceiver
void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
Executor executor;
...
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
mReceiver.onPageResult(mResultType, result);
}
});
} else {
mReceiver.onPageResult(mResultType, result);
}
}
注意这里的executor,这里就是为什么我们需要在loadInitial使用同步请求的原因
当我们调用刷新方法后,会重新执行一开始初始化的mRefreshRunnable
private final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};
public void invalidate() {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = mLiveData.hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
mExecutor.execute(mRefreshRunnable);
}
}
}
};
在compute的计算方法中,会将pageList重新实例化,会优先调用loadInitial方法进行初始化,实例化后,将结果返回给compute(),
后续在dispatchLoadInitial方法中会进行postExecutor的设置,如果loadInitial方法是异步的,postExecutor就会优先设置
如果不进行同步操作,会导致数据无法显示或者时刷新操作时提前清空了数据,导致显示不正常
- 回到上一步,调用dispatchResultToReceiver后会执行 mReceiver.onPageResult, mReceiver就是之前创建的ContiguousPagedList或TiledPagedList
onPageResult方法中根据PageResult的不同状态处理不同情况
private PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
public void onPageResult(@PageResult.ResultType int resultType,
@NonNull PageResult<V> pageResult) {
...
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);
}
}
};
void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
@NonNull Callback callback) {
init(leadingNulls, page, trailingNulls, positionOffset);
callback.onInitialized(size());
}
第一次显示列表状态为 PageResult.INIT,后续加载数据的状态为PageResult.APPEND,进行一些回调工作(onChanged,onInserted,onRemoved等)
- 由于在loadInitial方法中,我们的请求时同步的,所以会在数据处理结束后,View层的LiveData才会接受到数据,接受到数据后调用adapter.submitList(it)
- 列表初始显示、滑动或者notifyDataSetChanged时,会调用Adapter的getItem,然后委托给AsyncPagedListDiffer的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);
}
public void loadAround(int index) {
mLastLoad = index + getPositionOffset();
loadAroundInternal(index);
...
}
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();
}
}
mPagedList.loadAround(index)-loadAroundInternal(这里根据设置的prefetchDistance设置加载到多少item时去加载新数据)
schedulePrepend和scheduleAppend是分别调用before和after的两个方法
loadBefore和loadAfter的callBack调用在onPageResult方法中不再调用mStorage.init而是mStorage.appendPage
void appendPage(@NonNull List<T> page, @NonNull Callback callback) {
final int count = page.size();
...
callback.onPageAppended(mLeadingNullCount + mStorageCount - count,
changedCount, addedCount);
}
public void onPageAppended(int endPosition, int changedCount, int addedCount) {
...
// finally dispatch callbacks, after append may have already been scheduled
notifyChanged(endPosition, changedCount);
notifyInserted(endPosition + changedCount, addedCount);
}
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);
}
}
}
}
void notifyChanged(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.onChanged(position, count);
}
}
}
}
回调至AsyncPagedListDiffer的PagedList.Callback
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);
}
};
则是和Adapter绑定的callBack
public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mUpdateCallback = new AdapterListUpdateCallback(adapter);
mConfig = new AsyncDifferConfig.Builder<T>(diffCallback).build();
}
这样recyclerView就能接受到我们的数据变化