在平时使用RecyclerView
时,下拉刷新时先更新数据然后调用Adapter.notifyDataSetChanged
全量更新,修改条目时则先更新数据,然后调用Adapter.notifyItemXXX
进行局部更新。Paging
出现后,则只需要对数据进行变更,无需手动刷新UI,其内部会对数据源进行diff
操作(基于Myers 差分算法),然后选择合适的方式刷新UI,同时他还处理了数据的分页加载。本文主要结合Room
数据库进行使用和分析。
本文源码基于SDK 29
使用
引入依赖:
def paging_version = "2.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"
创建一个ViewModel
//PagingViewModel.java
private UserDao mUserDao; //dao对象用来从数据库中获取数据
private LiveData<PagedList<User>> mLiveData; //可观察的数据源
public LiveData<PagedList<User>> getLiveData() {
if (null == mLiveData && null != mUserDao) {
//room支持直接返回paging所需的数据源工厂类DataSource.Factory
DataSource.Factory<Integer, User> factory = mUserDao.queryUsersLive();
//配置参数
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(15) // 分页加载的数量
.setInitialLoadSizeHint(30) // 初次加载的数量
.setPrefetchDistance(10) // 预取数据的距离
.setEnablePlaceholders(false) // 是否启用占位符(本地数据比较合适,因为远程数据是未知的)
.build();
//用room返回的DataSource.Factory来构建数据列表
mLiveData = new LivePagedListBuilder<>(factory, config).build();
}
return mLiveData;
}
创建适配器MyListAdapter
继承自PagedListAdapter
,
//MyListAdapter.java
MyListAdapter() {
super(new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) {
//是否是同一个item,一般用数据源的唯一标识
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) {
//内容是否发生变更,一般重写equals
return oldItem.equals(newItem);
}
});
}
//创建ViewHolder,跟RecyclerView用法一样
UserAdapterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
RvItemPagingBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.rv_item_paging, parent, false);
return new UserAdapterHolder(binding);
}
//ViewHolder绑定数据,跟RecyclerView用法一样
public void onBindViewHolder(@NonNull UserAdapterHolder holder, int position) {
final User user = getItem(position);
holder.getBinding().setUser(user);
}
class UserAdapterHolder extends RecyclerView.ViewHolder {
private RvItemPagingBinding mBinding;
UserAdapterHolder(RvItemPagingBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
public RvItemPagingBinding getBinding() {
return mBinding;
}
}
在activity中使用,
//PagingActivity.java
onCreate(Bundle savedInstanceState) {
mViewModel.setUserDao(mUserDao);
mViewModel.getLiveData().observe(this, new Observer<PagedList<User>>() {
@Override
public void onChanged(PagedList<User> users) {
//提交数据
mListAdapter.submitList(users);
}
});
mBinding.rvUser.setAdapter(mListAdapter);
}
运行即可。
原理
下面将带着两个问题,逐步分析内部实现:
- submitList如何diff数据,刷新UI
- LivePagedListBuilder如何构建数据源
submitList如何diff数据,刷新UI
PagedListAdapter
是适配器,核心能力交给了AsyncPagedListDiffer
处理,AsyncPagedListDiffer
能对数据进行diff处理,
//AsyncPagedListDiffer.java
//构造方法
AsyncPagedListDiffer(RecyclerView.Adapter adapter,DiffUtil.ItemCallback<T> diffCallback) {
//把适配器包装到UpdateCallback
mUpdateCallback = new AdapterListUpdateCallback(adapter);
mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
}
来到AdapterListUpdateCallback
,
//AdapterListUpdateCallback.java
AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
void onInserted(int position, int count) {
//UpdateCallback在被触发时,将行为委托给适配器,这里就是熟悉的局部刷新代码了
mAdapter.notifyItemRangeInserted(position, count);
}
void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
那AdapterListUpdateCallback
又是什么时候被触发的呢,PagedListAdapter
调用submitList
时,委托给了AsyncPagedListDiffer
,
//AsyncPagedListDiffer.java
submitList(final PagedList<T> pagedList,final Runnable commitCallback) {
if (mPagedList == null && mSnapshot == null) {
//初始化的时候,直接这里回调,不走后面的差异计算
mUpdateCallback.onInserted(0, pagedList.size());
return;
}
//去子线程计算数据差异
final DiffUtil.DiffResult result =
PagedStorageDiffHelper.computeDiff(
oldSnapshot.mStorage, //老数据快照
newSnapshot.mStorage, //新数据快照
mConfig.getDiffCallback());
//回到主线程,把差异结果进行分发
latchPagedList(pagedList, newSnapshot, result,
oldSnapshot.mLastLoad, commitCallback);
}
latchPagedList(PagedList<T> newList,PagedList<T> diffSnapshot,DiffUtil.DiffResult diffResult,
int lastAccessIndex,Runnable commitCallback) {
//继续分发,这里传入了UpdateCallback
PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
previousSnapshot.mStorage, newList.mStorage, diffResult);
}
来到PagedStorageDiffHelper
,这里就可以看见AdapterListUpdateCallback
被回调了,具体的计算逻辑暂不深究,
//PagedStorageDiffHelper.java
dispatchDiff(ListUpdateCallback callback,final PagedStorage<T> oldList,
final PagedStorage<T> newList,final DiffUtil.DiffResult diffResult) {
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); //回调
}
}
总结一下,就是PagedListAdapter
调用submitList
,然后委托给AsyncPagedListDiffer
,其内部进行数据的差异计算,然后回调AdapterListUpdateCallback
使PagedListAdapter
调用notifyItemRangeXXX
进行局部刷新UI。
LivePagedListBuilder如何构建数据源
分析前需要先理解几个概念,PagedList
是具体的数据列表,由DataSource
数据源提供数据,DataSource
又由DataSource.Factory
工厂类创建。
首先来到LivePagedListBuilder.build()
,
//LivePagedListBuilder.java
LiveData<PagedList<Value>> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}
<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) {
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
private PagedList<Value> mList;
private DataSource<Key, Value> mDataSource;
@Override
protected PagedList<Value> compute() { //指定compute逻辑
@Nullable Key initializeKey = initialLoadKey;
if (mList != null) {
initializeKey = (Key) mList.getLastKey();
}
do {
//这里调了dataSourceFactory来创建dataSource
mDataSource = dataSourceFactory.create();
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());
return mList;
}
}.getLiveData();
}
先看mDataSource = dataSourceFactory.create()
这行,具体实现在生成类UserDao_Impl
,
//UserDao_Impl.java
@Override
public DataSource.Factory<Integer, User> queryUsersLive() {
final String _sql = "SELECT * FROM t_user";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
return new DataSource.Factory<Integer, User>() {
@Override
public LimitOffsetDataSource<User> create() {
//DataSource有多种类型,这里返回了LimitOffsetDataSource
return new LimitOffsetDataSource<User>(__db, _statement, false , "t_user") {
@Override
protected List<User> convertRows(Cursor cursor) {
//查询数据库,得到List<User>
final int _cursorIndexOfMId = CursorUtil.getColumnIndexOrThrow(cursor, "id");
final int _cursorIndexOfMName = CursorUtil.getColumnIndexOrThrow(cursor, "name");
final List<User> _res = new ArrayList<User>(cursor.getCount());
while(cursor.moveToNext()) {
final User _item;
_item = new User();
final int _tmpMId;
_tmpMId = cursor.getInt(_cursorIndexOfMId);
_item.setId(_tmpMId);
final String _tmpMName;
_tmpMName = cursor.getString(_cursorIndexOfMName);
_item.setName(_tmpMName);
_res.add(_item);
}
return _res;
}
};
}
};
}
在LimitOffsetDataSource
里,通过convertRows
的实现从数据库拿到list,
//LimitOffsetDataSource.java
void loadInitial(LoadInitialParams params,LoadInitialCallback<T> callback) {
//计算查询范围
sqLiteQuery = getSQLiteQuery(firstLoadPosition, firstLoadSize);
//得到游标
cursor = mDb.query(sqLiteQuery);
List<T> rows = convertRows(cursor);
//拿到list后,就可以进行diff操作了
list = rows;
callback.onResult(list, firstLoadPosition, totalCount);
}
优缺点
- TODO
参考文章
本文使用 mdnice 排版