In normal times, when using RecyclerView drop-down refresh to update the data first and then calling Adapter. The notifyDataSetChanged full quantity update, modify the entries is to update the data first, and then call Adapter. NotifyItemXXX update locally. After Paging appears, it only needs to change data without manually refreshing UI. It will perform diff operation on data source internally (based on Myers difference algorithm) and then select an appropriate way to refresh UI. Meanwhile, Paging loading of data is also processed. This paper mainly uses and analyzes Room database.
Jetpack note code
The source of this article is based on SDK 29
use
Introducing dependencies:
def paging_version = 2.1.1 ""
implementation "androidx.paging:paging-runtime:$paging_version"
Copy the code
Create a ViewModel
//PagingViewModel.java
private UserDao mUserDao; // The DAO object is used to retrieve data from the database
private LiveData<PagedList<User>> mLiveData; // Observable data source
public LiveData<PagedList<User>> getLiveData() {
if (null == mLiveData && null! = mUserDao) {// Room supports directly returning the DataSource Factory class datasource.factory required for Paging
DataSource.Factory<Integer, User> factory = mUserDao.queryUsersLive();
// Configure parameters
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(15) // The number of page loads
.setInitialLoadSizeHint(30) // The number of initial loads
.setPrefetchDistance(10) // The distance to prefetch data
.setEnablePlaceholders(false) // Whether to enable placeholders (local data is better, since remote data is unknown)
.build();
// Use the datasource. Factory returned by room to build the data list
mLiveData = new LivePagedListBuilder<>(factory, config).build();
}
return mLiveData;
}
Copy the code
Create adapter MyListAdapter inherited from PagedListAdapter,
//MyListAdapter.java
MyListAdapter() {
super(new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) {
// Whether it is the same item is usually identified by the unique identifier of the data source
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) {
// Whether the content has changed, usually override equals
returnoldItem.equals(newItem); }}); }
// Create ViewHolder as 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);
}
// Bind data to ViewHolder as 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(a) { returnmBinding; }}Copy the code
Used in an activity,
//PagingActivity.java
onCreate(Bundle savedInstanceState) {
mViewModel.setUserDao(mUserDao);
mViewModel.getLiveData().observe(this.new Observer<PagedList<User>>() {
@Override
public void onChanged(PagedList<User> users) {
// Submit datamListAdapter.submitList(users); }}); mBinding.rvUser.setAdapter(mListAdapter); }Copy the code
Run.
The principle of
The following steps through the internal implementation with two questions:
- How does submitList diff data and refresh the UI
- How does LivePagedListBuilder build a data source
How does submitList diff data and refresh the UI
PagedListAdapter is an adapter that allows AsyncPagedListDiffer to perform diff processing on data.
//AsyncPagedListDiffer.java
// constructor
AsyncPagedListDiffer(RecyclerView.Adapter adapter,DiffUtil.ItemCallback<T> diffCallback) {
// Wrap the adapter into UpdateCallback
mUpdateCallback = new AdapterListUpdateCallback(adapter);
mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
}
Copy the code
Come to AdapterListUpdateCallback,
//AdapterListUpdateCallback.java
AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
void onInserted(int position, int count) {
// When UpdateCallback is triggered, it delegates the behavior to the adapter, which is the familiar local refresh code
mAdapter.notifyItemRangeInserted(position, count);
}
void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
Copy the code
What AdapterListUpdateCallback when triggered, PagedListAdapter call submitList, entrusted to the AsyncPagedListDiffer,
//AsyncPagedListDiffer.java
submitList(final PagedList<T> pagedList,final Runnable commitCallback) {
if (mPagedList == null && mSnapshot == null) {
// When initializing, call back directly here, do not go behind the difference calculation
mUpdateCallback.onInserted(0, pagedList.size());
return;
}
// Go to the child thread to calculate the data difference
final DiffUtil.DiffResult result =
PagedStorageDiffHelper.computeDiff(
oldSnapshot.mStorage, // Old data snapshot
newSnapshot.mStorage, // New data snapshot
mConfig.getDiffCallback());
// go back to the main thread and distribute the difference result
latchPagedList(pagedList, newSnapshot, result,
oldSnapshot.mLastLoad, commitCallback);
}
latchPagedList(PagedList<T> newList,PagedList<T> diffSnapshot,DiffUtil.DiffResult diffResult,
int lastAccessIndex,Runnable commitCallback) {
// To continue distribution, UpdateCallback is passed in
PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
previousSnapshot.mStorage, newList.mStorage, diffResult);
}
Copy the code
Come to PagedStorageDiffHelper, can see AdapterListUpdateCallback callback, temporarily not get specific calculation logic,
//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); / / callback
return;
}
// First, remove or insert trailing nulls
if (trailingOld > trailingNew) {
int count = trailingOld - trailingNew;
callback.onRemoved(oldList.size() - count, count); / / callback
} else if (trailingOld < trailingNew) {
callback.onInserted(oldList.size(), trailingNew - trailingOld); / / callback
}
// Second, remove or insert leading nulls
if (leadingOld > leadingNew) {
callback.onRemoved(0, leadingOld - leadingNew); / / callback
} else if (leadingOld < leadingNew) {
callback.onInserted(0, leadingNew - leadingOld); / / callback}}Copy the code
PagedListAdapter calls submitList and delegates AsyncPagedListDiffer internally to perform data divergence calculations. Then the callback AdapterListUpdateCallback make PagedListAdapter call notifyItemRangeXXX local refresh the UI.
How does LivePagedListBuilder build a data source
A PagedList is a concrete list of data provided by the DataSource DataSource and created by the DataSource.Factory class.
First came to LivePagedListBuilder. The 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(a) { // Specify the compute logic
@Nullable Key initializeKey = initialLoadKey;
if(mList ! =null) {
initializeKey = (Key) mList.getLastKey();
}
do {
// Call dataSourceFactory to create 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();
}
Copy the code
MDataSource = datasourceFactory.create (); UserDao_Impl
//UserDao_Impl.java
@Override
public DataSource.Factory<Integer, User> queryUsersLive(a) {
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(a) {
//DataSource has multiple types. LimitOffsetDataSource is returned
return new LimitOffsetDataSource<User>(__db, _statement, false , "t_user") {
@Override
protected List<User> convertRows(Cursor cursor) {
List
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; }}; }}; }Copy the code
In the LimitSet datasource, get the list from the database via the convertRows implementation,
//LimitOffsetDataSource.java
void loadInitial(LoadInitialParams params,LoadInitialCallback<T> callback) {
// Calculate the query range
sqLiteQuery = getSQLiteQuery(firstLoadPosition, firstLoadSize);
// Get the cursor
cursor = mDb.query(sqLiteQuery);
List<T> rows = convertRows(cursor);
// Get the list and diff
list = rows;
callback.onResult(list, firstLoadPosition, totalCount);
}
Copy the code
The advantages and disadvantages
- TODO
Refer to the article
- Design aesthetics of Paging: The Paging library
This article is formatted using MDNICE