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,RemoteMediator
It’s also a single data source, it’s going to be inPagingSource
Use only when no data is availableRemoteMediator
Provided data, if there is both a database request and a network request, usuallyPagingSource
For database requests,RemoteMediator
Make 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 pagesRecyclerView
Adapter.
PagingSource and RemoteMediator act as data sources. The ViewModel listens for data refreshes using the Flow
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.
- provide
DiffUtil.ItemCallback<Shoe>
- inheritance
PagingDataAdapter
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