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