preface

The Paging library is a new Google jetPack component that encapsulates Paging logic. After using the Paging library, you do not need to consider the logic of loading the next page. In addition, you can automatically load the page and conveniently observe the status of loading the next page

This article mainly includes the basic use of Paging3 and part of the source code analysis, as follows: 1. The basic use of Paging3 2.Paging3 automatic loading more principle

Basic use of Paging3

The main advantages

The Paging library contains the following functions:

  • In-memory caching of paging data. This ensures that your application uses system resources efficiently when working with paging data.
  • The built-in request deduplication function ensures that your application uses network bandwidth and system resources efficiently.
  • Configurable RecyclerView adapters that automatically request data when the user scrolls to the end of the loaded data.
  • First-class support for Kotlin coroutines and processes, as well as LiveData and RxJava.
  • Built-in support for error handling, including refresh and retry capabilities.

The basic structure

There are several classes that are useful:

  • PagingSource: single data source.
  • RemoteMediator: in fact,RemoteMediatorIt’s also a single data source, it’s going to be inPagingSourceUse only when no data is availableRemoteMediatorProvided data, if there is both a database request and a network request, usuallyPagingSourceFor database requests,RemoteMediatorMake a network request.
  • PagingData: container for single-page data.
  • Pager: used to constructFlow<PagingData>Class to implement the data load completion callback.
  • PagingDataAdapter: loads data in pagesRecyclerViewAdapter.

PagingSource and RemoteMediator act as data sources. The ViewModel listens for data refreshes using the Flow provided in Pager. Every time RecyclerView is about to scroll to the bottom, new data will arrive. Finally, the PagingAdapter displays the data.

The basic use

1. Configure the data source

First you need to generate the data layer and configure the data source

private const val SHOE_START_INDEX = 0;

class CustomPageDataSource(private val shoeRepository: ShoeRepository) : PagingSource<Int, Shoe>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Shoe> {
        valpos = params.key ? : SHOE_START_INDEXval startIndex = pos * params.loadSize + 1
        val endIndex = (pos + 1) * params.loadSize
        return try {
            // Remove data from the database
            val shoes = shoeRepository.getPageShoes(startIndex.toLong(), endIndex.toLong())
            // Return your pagination results and fill in the previous and the next keys
            LoadResult.Page(
                shoes,
                if (pos <= SHOE_START_INDEX) null else pos - 1.if (shoes.isNullOrEmpty()) null else pos + 1)}catch (e:Exception){
            LoadResult.Error(e)
        }
        
    }
}
Copy the code

2. Generate observable data sets

The second step is to generate Observable data sets in the viewModel. Observable data sets include LiveData, Flow, Observable and Flowable in RxJava, which need to be supported separately by the extension library.

class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {

    / * * *@paramConfig page parameters *@paramPagingSourceFactory Factory for a single data source, providing a PageSource * in the closure@paramRemoteMediator supports data sources for both network requests and database requests@paramInitialKey Key used for initialization
    var shoes = Pager(config = PagingConfig(
        pageSize = 20
        , enablePlaceholders = false
        , initialLoadSize = 20
    ), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow

    / /... omit
}
Copy the code

3. Create a Adapter

There are no major differences between the Adapter and the ordinary Adapter.

  • provideDiffUtil.ItemCallback<Shoe>
  • inheritancePagingDataAdapter

To use PagingAdapter, you need to implement the DiffUtil.ItemCallback interface, because the DiffUtil interface determines whether the data is the same when submitting the data, so as to insert and delete the data

4. Use it in the UI

If only data is displayed, here’s what we do:

  • Create and set up the adapter.
  • Start a coroutine
  • Receive data provided by Flow in coroutines.
valadapter = ShoeAdapter(context!!) binding.recyclerView.adapter = adapter job = viewModel.viewModelScope.launch(Dispatchers.IO) { viewModel.shoes.collect()  { adapter.submitData(it) } }Copy the code

5. Monitor the data loading status

Paging can monitor the loading state of data. The corresponding class of the state is LoadState, which has three states:

  • Loading: Data is being loaded.
  • NotLoading: Memory has acquired data, and Paging does not need to request more data even if it goes down.
  • Error: An Error was returned while requesting data.

Code to listen for data status:

adapter.addLoadStateListener {state:CombinedLoadStates->
    / /... State monitoring
}
Copy the code

Loadstates: loadStates: loadStates: loadStates: loadStates: loadStates: loadStates: loadStates: loadStates: loadStates

  • refresh:LoadState: State when refreshed as it can be calledPagingDataAdapter#refresh()Method to refresh data.
  • append:LoadState: Can be understood as the request state of data when RecyclerView is sliding.
  • prepend:LoadState: can be understood as the request state of data when RecyclerView slides up.

How does Paging3 automatically load more?

When RecyclerView is about to roll to the bottom, Paging library will automatically load more, we can see how to implement it

In fact, more of the logic for Paging to load is triggered by the getItem() method of PagingDataAdapter

protected fun getItem(@IntRange(from = 0) position: Int) = differ.getItem(position)
Copy the code

Here differ is an AsyncPagingDataDiffer object:

fun getItem(@IntRange(from = 0) index: Int): T? {
        try {
            inGetItem = true
            return differBase[index]
        } finally {
            inGetItem = false}}Copy the code

Next, look at the get() method for differBase, which is a PagingDataDiffer object:

operator fun get(index: Int): T? { lastAccessedIndex = index receiver? .addHint(presenter.loadAround(index))return presenter.get(index)
    }
Copy the code

Here, receiver is a UiReceiver object that will be initialized as PagerUiReceiver when initialized. Now look at the addHint() method

inner class PagerUiReceiver<Key : Any, Value : Any> constructor(
        private val pageFetcherSnapshot: PageFetcherSnapshot<Key, Value>,
        private val retryChannel: SendChannel<Unit>
    ) : UiReceiver {
        override fun addHint(hint: ViewportHint) = pageFetcherSnapshot.addHint(hint)

        override fun retry(a) {
            retryChannel.offer(Unit)}override fun refresh(a) = this@PageFetcher.refresh()
    }
Copy the code

PageFetcherSnapshot is a pageFetcherSnapshot object

fun addHint(hint: ViewportHint) {
        lastHint = hint
        @OptIn(ExperimentalCoroutinesApi::class)
        hintChannel.offer(hint)
    }
Copy the code

HintChannel is a BroadcastChannel object, and as long as there is a new value in this channel, it will broadcast to all subscribers. Let’s see where to subscribe to hintChannel

hintChannel.asFlow()
            // Prevent infinite loop when competing PREPEND / APPEND cancel each other
            .drop(if (generationId == 0) 0 else 1)
            .map { hint -> GenerationalViewportHint(generationId, hint) }
    }
        // Prioritize new hints that would load the maximum number of items.
        .runningReduce { previous, next ->
            if (next.shouldPrioritizeOver(previous, loadType)) next else previous
        }
        .conflate()
        .collect { generationalHint ->
            doLoad(loadType, generationalHint)
        }
Copy the code

As you can see, when the upstream hintChannel has a value, a GenerationalViewportHint object is constructed, and the downstream doLoad() method is called:

private suspend fun doLoad(
        loadType: LoadType,
        generationalHint: GenerationalViewportHint
    ){...varloadKey: Key? = stateHolder.withLock { state -> state.nextLoadKeyOrNull( loadType, generationalHint.generationId, generationalHint.presentedItemsBeyondAnchor(loadType) + itemsLoaded, )? .also { state.setLoading(loadType) } } ...loop@ while(loadKey ! =null) {
            val params = loadParams(loadType, loadKey)
            valresult: LoadResult<Key, Value> = pagingSource.load(params) .... }}Copy the code

You can see that when loadKey is not null, the load method of pagingSource is called to load the next page. Let’s look at nextLoadKeyOrNull

private fun PageFetcherSnapshotState<Key, Value>.nextLoadKeyOrNull(
        loadType: LoadType,
        generationId: Int,
        presentedItemsBeyondAnchor: Int
    ): Key? {
        if(generationId ! = generationId(loadType))return null
        // Skip load if in error state, unless retrying.
        if (sourceLoadStates.get(loadType) is Error) return null

        // Skip loading if prefetchDistance has been fulfilled.
        if (presentedItemsBeyondAnchor >= config.prefetchDistance) return null

        return if (loadType == PREPEND) {
            pages.first().prevKey
        } else {
            pages.last().nextKey
        }
    }
Copy the code

As can be seen from the above, nextKey will be returned only when the loading status is successful and the distance of the last one is less than the pre-loading distance, that is, the next page will be loaded

The above is the source code analysis of Paing3 automatically loading the next page, summarized as the sequence diagram as follows:

The resources

Android Jetpack-Paging 3 Internal principles of Paging