目录
  1. 1. 简介
  2. 2. Paging 工作原理
    1. 2.1. 官方工作原理图示
    2. 2.2. Paging的工作流程
  3. 3. Paging 使用
  4. 4. 列表数据差异增量更新
    1. 4.1. UML图解 Paging 工作流程
  5. 5. Paging 优缺点
重拾Android-JetPack全家桶(六)之Paging分页库

相信很多开发者在项目实战中,经常会用到列表页面分页显示、加载更多等功能。Jetpack架构组件中Paging,就是将这部分的工作简化,使之搭配LiveData能够快速的完成页面初始化数据、分页数据的加载。

简介

什么是Paging?

Paging 组件是谷歌新推出的分页组件,可以轻松的帮助开发者实现RecyclerView中分页预加载以达到无限畅滑的效果。

Paging 工作原理

官方工作原理图示

微信图片_20210118161937.jpg

1、DataSource 是数据源提供者,数据的改变会驱动列表的更新,因此数据源是跟重要的。这里一共有3种DataSource可选,取决于数据是以何种方式分页加载的。

  • ItemKeyedDataSource: 基于cursor游标实现,数据容量可动态自增;
  • PageKeyedDataSource: 基于页码实现,数据容量可动态自增;
  • PositionalDataSource: 数据容量固定,基于index索引加载特定范围的数据。

2、PagedList 是核心类,它驱动Paging从数据源加载数据,同时负责 页面数据初始化分页数据何时加载以何种方式加载

3、PagedListAdapter 是列表适配器,通过DiffUtil差分异定向更新列表数据。

Paging的工作流程

当DataSource加载出页面数据 —> 把数据转交给 PagedList 准备刷新页面 —> PagedListAdapter 数据差分异 —> 定向刷新列表Item

Paging 使用

受限使用之前添加依赖项

implementation 'androidx.paging:paging-runtime:2.1.0'

Paging的设计和传统的分页加载不一样,它巧妙的结合了LiveData的功能。先看如何使用Paging触发页面初始化数据:

  • 1) 构建PagedList.Config 对象,用以声明以何种方式分页
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(10) // 指定每次分页加载的条目数量
.setInitialLoadSizeHint(20) // 指定初始化数据加载的条目数量
.setPrefetchDistance(10) // 指定提前分页预加载的条目数量,默认和pageSize的值相等
.setMaxSize(50) // 指定数据源最大可加载的条目数量
.setEnablePlaceholders(true) // 指定未加载出来的条目是否使用占位项替代,必须和setMaxSize搭配使用才有效
.build(); // 构造出配置对象,用以指定PagedList以何种方式分页
  • 2)创建数据源工厂类,用来创建数据提供者
DataSource.Factory factory = new DataSource.Factory() {
@NonNull
@Override
public DataSource create() {
return new ItemKeyedDataSource() {
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) {

}

@Override
public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) {

}

@Override
public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) {

}

@NonNull
@Override
public Object getKey(@NonNull Object item) {
return null;
}
};
}
};
  • 3)构建一个能触发页面加载初始化数据的LiveData对象
LiveData<PagedList<Object>> pageData = new LivePagedListBuilder<>(factory, config)
.setInitialLoadKey(0) // 设置初始化数据加载的key(任意类型)
//.setFetchExecutor() // 指定使用哪个线程池进行异步工作
//.setBoundaryCallback(callback) // 指定pagedList第一条、最后一条,被加载到列表之上的边界回调callback
.build(); //最后构建出LiveData对象,其泛型是<PagedList<T>
  • 4)最后只需拿到前面构建出来的 LiveData 对象注册一个Observer观察者即可触发页面初始化加载
mViewModel.getPageData().observe(this, new Observer<PagedList<T>>() {
@Override
public void onChanged(PagedList<T> pagedList) {
mAdapter.submitList(pagedList);
}
});

那么Paging 是如何利用 LiveData 这个功能组件的,首先看下 ComputableLiveData 这个类,看源码它并不是 LiveData 的子类,但是它利用了 LiveDataonActive 方法被激活的时机。分析如下:

public abstract class ComputableLiveData<T> {

public ComputableLiveData(@NonNull Executor executor) {
mExecutor = executor;
// 在构造函数中,创建了一个LiveData对象,并且覆写了它的 onActive 方法
// 该方法当且仅当有第一个Observer被注册到LiveData,就会被调用,
// 而当 onActive 被调用,它使用线程池执行了RefreshRunnable,实际上就是触发了下面 compute 方法
mLiveData = new LiveData<T>() {
@Override
protected void onActive() {
mExecutor.execute(mRefreshRunnable);
}
};
}

// 虚方法,获取在构造函数中创建的 LiveData 对象
@SuppressWarnings("WeakerAccess")
@NonNull
public LiveData<T> getLiveData() {
return mLiveData;
}

/**
* 一旦调用该方法,也会触发下面 compute 方法
*/
public void invalidate() {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}

// 虚方法,在 LivePagedListBuilder 中有唯一实现
@SuppressWarnings({"WeakerAccess", "UnknownNullness"})
@WorkerThread
protected abstract T compute();
}

而这也就解释了为什么下面这段代码可以触发 Paging 的初始化数据的加载逻辑。

接下来再来看上面的 LivePagedListBuilder#build() 方法又是如何使用 ComputableLiveData 类的

public LiveData<PagedList<Value>> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}

private static <Key, Value> LiveData<PagedList<Value>> create(
@Nullable final Key initialLoadKey,
@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) {
// 该方法直接new 出ComputableLiveData,并覆写了它的 compute 方法,很重要
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() {
// 一旦监听到 DataSource 被置为无效,则调用ComputableLiveData#invalidate 方法
// 也就是会触发下面的 compute 方法
invalidate();
}
};

@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList<Value> compute() {
@Nullable Key initializeKey = initialLoadKey;
if (mList != null) {
initializeKey = (Key) mList.getLastKey();
}

do {
if (mDataSource != null) {
mDataSource.removeInvalidatedCallback(mCallback);
}

// 通过 dataSourceFactory 创建数据源提供者
mDataSource = dataSourceFactory.create();
// 并且给数据源注册一个回调,用以监听数据源被置为无效的情境
// 一旦 DataSource 被置为无效,则不能在提供数据,但会再次触发该(compute)方法,并再次创建一个新的数据源 DataSource,重新执行下面的逻辑
mDataSource.addInvalidatedCallback(mCallback);

// compute 方法是 ComputableLiveData 的方法
// 会通过 PagedList.Builder 的build方法构造出一个PagedList
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());
return mList;
}
}.getLiveData();
}

接下来进入 PagedList 的 build 方法

public PagedList<Value> build() {
... ...

return PagedList.create(
mDataSource,
mNotifyExecutor,
mFetchExecutor,
mBoundaryCallback,
mConfig,
mInitialKey);
}

进入 create 方法

static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> 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<K, T>) ((PositionalDataSource<T>) dataSource)
.wrapAsContiguousWithoutPlaceholders();
if (key != null) {
lastLoad = (Integer) key;
}
}
ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
(key != null) ? (Integer) key : 0);
}
}

触发初始化数据的加载

ContiguousPagedList.java

ContiguousPagedList(
@NonNull ContiguousDataSource<K, V> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<V> boundaryCallback,
@NonNull Config config,
final @Nullable K key,
int lastLoad) {
super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
boundaryCallback, config);
mDataSource = dataSource;
mLastLoad = lastLoad;

// 如果当前DataSource已经被置为无效了,那么就不会触发初始化数据加载的逻辑
if (mDataSource.isInvalid()) {
detach();
} else {
// 这里触发了上面配置的 DataSource 的 loadInitial 方法,也就开始页面数据初始化加载
// 其中 mReceiver 参数,这是接收网络数据加载成功之后的callback
// PageResult。Receiver 是内部类,它会判断本次分页回来的数据是 初始化数据,还是分页数据,用以区分分页的状态
// 而数据最终都会被存储到 PagedStorage<T> 这个类中,它实际上是一个按页存储数据的ArrayList类型类
mDataSource.dispatchLoadInitial(key,
mConfig.initialLoadSizeHint,
mConfig.pageSize,
mConfig.enablePlaceholders,
mMainThreadExecutor,
mReceiver);
}
mShouldTrim = mDataSource.supportsPageDropping()
&& mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
}

触发分页数据的加载

分页数据加载的逻辑是在 PagedListAdapter#getItem() 方法中触发的,接着又会调用 ContiguousPagedList#loadAroundInternal 方法。该方法会计算列表当前滑动状态下列表后面还需要追加几条 Item,列表前面还需要向前追加几条 Item,Paging 能向前向后分页加载数据,可谓是相当强大。

ContiguousPagedList.java

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) {
// 计算之后,如果需要向前追加的 Item size >0,schedulePrepend则会触发 DataSource 的 LoadBefore 方法
schedulePrepend();
}

mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
// 计算之后,如果需要向后追加的 Item size >0,scheduleAppend则会触发 DataSource 的 LoadAfter 方法
scheduleAppend();
}
}

以上就是Paging分页加载数据的工作原理,但是还是要付诸实践,毕竟实践出真知。

列表数据差异增量更新

既然使用Paging分页组件,就得按照开发框架来应用。给列表RecyclerView设置Adapter需要使用 PagedListAdapter,并且要求传递一个 DiffUtil.ItemCallback 用以做列表新旧数据的差分异计算。这个思想很重要,很多开源框架用到,比如:Tinker的插件更新机制。有了这个差分异计算,便能使用Paging提供的列表数据差量更新能力了。

public class CategoryAdapter extends PagedListAdapter<Category, RecyclerView.ViewHolder> {

protected CategoryAdapter() {
super(new DiffUtil.ItemCallback<Category>() {
@Override
public boolean areItemsTheSame(@NonNull Category oldItem, @NonNull Category newItem) {
return oldItem.getId() == newItem.getId();
}

@Override
public boolean areContentsTheSame(@NonNull Category oldItem, @NonNull Category newItem) {
return oldItem.equals(newItem);
}
});
}

@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return null;
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {

}
}

UML图解 Paging 工作流程

微信图片_20210118203051.jpg

Paging 优缺点

虽然Paging的设计十分优秀,功能也很强大,但是现阶段版本仍然存在不足,有一些问题 Paging并没有给出合理的解决方案,比如:

  • 1、PagedList 并不支持列表数据的增删改
  • 2、Paging 一旦有一次分页失败,便再也不会继续分页,因为数据源已经失效;
  • 3、Paging 如何先展示缓存数据再展示网络数据,也是个问题;
  • 4、Paging 如果先添加了 HeaderView,再展示加载的网络数据,列表会自动往下滑动;
打赏
  • 微信
  • 支付宝

评论