Paging

An overview of the

Jetpack’s paging library helps developers better separate UI and data capture logic, reducing project coupling. This article focuses on getting data directly from the server

Project address: github.com/Tkorn/kotli…

Library architecture

  • DataSource.Factory, as the name implies, is the DataSource Factory. An abstract class that requires us to implement the create() method that returns the DataSource object. ** Note: create() should return a new object every time it is called, otherwise invalidate() will not refresh the list and will loop endlessly. (For reasons explained later)
    fun createDataSourceFactory(dataSource: DataSource<Long, UserBean>):DataSource.Factory<Long, UserBean>{
        return object: DataSource.Factory<Long,UserBean>(){
            override fun create(): DataSource<Long, UserBean> {
                return dataSource
            }
        }
    }
Copy the code
  • DataSource, Paging already provides us with three very comprehensive implementations:

    • PageKeyedDataSource: Obtains data from the key associated with the current page. It is very common for the key to be the size of the requested page.
    • ItemKeyedDataSource: Obtains data from the next page using the item as the key. For example, in a chat session, the request for the next page of data might require the ID of the previous piece of data.
    • PositionalDataSource: Obtains data from the next page by using the position in the data as the key. This is typically used in the Database as described above.

    ItemKeyedDataSource = ItemKeyedDataSource = ItemKeyedDataSource ItemKeyedDataSource is also an abstract class that requires us to implement four methods:

    • LoadInitial (LoadInitialParams params, LoadInitialCallback callback) load the initial data (first page)
    • LoadAfter (LoadParams params,LoadCallback callback) Loads the data on the next page
    • LoadBefore (LoadParams params,LoadCallback callback) loadBefore(LoadParams,LoadCallback callback) loadBefore(LoadParams,LoadCallback callback) loadBefore(LoadParams,LoadCallback callback) loadBefore(LoadParams,LoadCallback callback) loadBefore(LoadParams,LoadCallback callback) If you don’t need it, it’s empty
    • Key getKey(Value item)
  • The key component of the paging library is the PagedList class, which is used to load application data blocks or pages. As more data is needed, it is paged into an existing PagedList object. If any of the loaded data changes, a new PagedList instance is emitted to the observable data store from either LiveData or an RxJava2 based object. As the PagedList object is generated, the application interface renders its contents, taking into account the life cycle of the interface controls. Can be created with LivePagedListBuilder

    val userLiveData =
        LivePagedListBuilder(mRepository.createDataSourceFactory(createDataSource()),
            mRepository.createConfig())
            .setInitialLoadKey(1)
            .build()
Copy the code

LivePagedListBuilder(DataSource.Factory<Key, Value> dataSourceFactory, PagedList.Config config)

The LivePagedListBuilder requires datasource.factory (described above) and pagedList.config (which provides parameters for paging).

Config(int pageSize, // how many pages to load int prefetchDistance,// how many pages to load the request for the next page data BooleanenablePlaceholder int initialLoadSizeHint, // how much data will be loaded first int maxSize // How much data will be stored, for a placeholder, or for a placeholder. }Copy the code
  • PagedListAdapter, and RecyclerView.Adapter use is not different, just getItemCount and getItem rewrite, because it uses DiffUtil, to avoid useless update data.
class PagingAdapter : PagedListAdapter<ArticleModel, PagingVH>(diffCallbacks) {
 
    companion object {
        private val diffCallbacks = object : DiffUtil.ItemCallback<ArticleModel>() {

            override fun areItemsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem.id == newItem.id
 
            override fun areContentsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem == newItem

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingVH = PagingVH(R.layout.item_paging_article_layout, parent)
 
    override fun onBindViewHolder(holder: PagingVH, position: Int) = holder.bind(getItem(position))
}
Copy the code

Once the PagedList is observed, the submitList is passed to the Adapter.

viewModel.userList.observe(this, Observer {
    adapter.submitList(it)
})
Copy the code

See my Demo for details, using MVVM + LiveData + Koin + coroutine + Retrofit. The project address is at the top

The problem

  • The reason why create() of the datasource.factory needs to return a new object every time it is called is because when invalidate() is called, the DataSource’s mInvalid will be set to true, and then the onInvalidated() call is made.
    // Source @anyThread public voidinvalidate() {
        if (mInvalid.compareAndSet(false.true)) {
            for(InvalidatedCallback callback : mOnInvalidatedCallbacks) { callback.onInvalidated(); }}}Copy the code

LivePagedListBuilder the create () rewrite the DataSource. InvalidatedCallback

//LivePagedListBuilder source @anyThread @nonnull @suppressLint ()"RestrictedApi")
    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) {
        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() { 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); } mDataSource = dataSourceFactory.create(); mDataSource.addInvalidatedCallback(mCallback); mList = new PagedList.Builder<>(mDataSource, config) .setNotifyExecutor(notifyExecutor) .setFetchExecutor(fetchExecutor) .setBoundaryCallback(boundaryCallback) .setInitialKey(initializeKey) .build(); }while (mList.isDetached());
                returnmList; } }.getLiveData(); }}Copy the code

As you can see, mlist.isdetached () = true goes into an infinite loop. Its default value is false. MDataSource = datasourcefactory.create (); The mDataSource is reacquired, and then the pagedList. Builder is used to retrieve the new mList data.

    @WorkerThread
    TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback<T> boundaryCallback,
            @NonNull Config config,
            int position) {
        super(new PagedStorage<T>(), mainThreadExecutor, backgroundThreadExecutor,
                boundaryCallback, config);
        mDataSource = dataSource;

        final int pageSize = mConfig.pageSize;
        mLastLoad = position;

        if (mDataSource.isInvalid()) {
            detach();
        } else {
            final int firstLoadSize =
                    (Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize;

            final int idealStart = position - firstLoadSize / 2;
            final int roundedPageStart = Math.max(0, idealStart / pageSize * pageSize);

            mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize, pageSize, mMainThreadExecutor, mReceiver); }}Copy the code

Mdatasource.isinvalid () ¶ If our DataSource’s create() method returns not the new object but the previous one, The mDataSource has called invalidate() and therefore goes to the detach() method without fetching the new data.

/ / source @ SuppressWarnings ("WeakerAccess")
    public void detach() {
        mDetached.set(true);
    }
Copy the code

Can see detach () take mDetached the true, this would lead to mList. IsDetached () is true, when get LivePagedListBuilder the create () method will be infinite loop.