Android-Jetpack笔记-Paging结合数据库

2,038 阅读4分钟

在平时使用RecyclerView时,下拉刷新时先更新数据然后调用Adapter.notifyDataSetChanged全量更新,修改条目时则先更新数据,然后调用Adapter.notifyItemXXX进行局部更新。Paging出现后,则只需要对数据进行变更,无需手动刷新UI,其内部会对数据源进行diff操作(基于Myers 差分算法),然后选择合适的方式刷新UI,同时他还处理了数据的分页加载。本文主要结合Room数据库进行使用和分析。

Jetpack笔记代码

本文源码基于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 排版