preface
Use more or less encountered some problems, to turn over the source code found that Paging3 implementation involves a lot of coroutine content, but their own coroutine is also a little understanding, so the views and solutions may be wrong or unreasonable place.
Simple to use
While there are few tutorials available online at the time of this writing, the few that are available are very detailed. Paging 3 Library Overview Paging 3 Library Overview Paging 3 library Overview Paging 3 library Overview Paging 3 library Overview
Note: There is a bug in paging3 that calls to the Retry method may not trigger retries when more loads fail. The specific reason is not known in detail, the bug has been fixed in 3.0.0-alpha05, and this article is also based on 3.0.0-alpha05.
What Paging3 involves
Paging3 has a considerable change from the previous version of Paging. I also used Paging2 slightly before (in my previous practice project, there were still many pits after I finished writing, and I planned to reconstruct the project after the pits were filled), but I only knew a little about it. As a result, there were not many pits in the project, and some problems encountered were also difficult to solve. Using Paging3 this time, I intend to take care of the potholes I encounter and understand the library as best I can.
Pager
The Pager is mainly used to build the PagingData that the flow provides to the PagingDataAdapter. It contains very little code, mainly through PageFetcher to create flow, the specific logic code is in PageFetcher.
PagingConfig
Some basic paging information, such as page number and size to load, is passed by LoadParams in the Load method of PagingSource.
PagingSource
Inherit the class and implement the load method to load the data, returning loadResult. Page or loadResult. Error depending on the load. Load the previous page, next page, or refresh through this method, noting the suspend modifier. The load state can be obtained through the addLoadStateListener method of PagingDataAdapter or the loadStateFlow variable.
PagingDataAdapter
In addition to a normal Adapter, an instance of DiffUtil.ItemCallback is provided to compare the updated list of data.
Pit point 1, how to do the page hopping function
Paging3 works on its own after the Pager is built and the data is observed, automatically loading the top or bottom data according to PagingConfig. What to do when you need to jump to a page? There is no way to jump to a specific page in the above component.
If you can’t find the API, go to the source code. As mentioned in the Pager class comment, each PagingData represents a snapshot that supports PagingData, and a new instance of PagingData should be provided when the data is refreshed. From this description you can see that the page-hopping function needs to provide a new instance of PagingData.
So when is the PagingData instance provided, and there’s an implementation in the PageFetcher class
val flow: Flow<PagingData<Value>> = channelFlow {
refreshChannel.asFlow()
.onStart {
/ /...
}
.scan(null) { previousGeneration: PageFetcherSnapshot<Key, Value>? , triggerRemoteRefresh ->/ /...
PageFetcherSnapshot(
initialKey = initialKey,
pagingSource = pagingSource,
config = config,
retryFlow = retryChannel.asFlow(),
triggerRemoteRefresh = triggerRemoteRefresh,
remoteMediatorAccessor = remoteMediatorAccessor,
invalidate = this@PageFetcher::refresh
)
}
.filterNotNull()
.mapLatest { generation ->
PagingData(generation.pageEventFlow, PagerUiReceiver(generation, retryChannel))
}
.collect { send(it) }
}
Copy the code
You can see that a paging snapshot is created in flow, and finally PagingData is created from the object’s pageEventFlow and an instance of PagerUiReceiver and sent out for external use. PageEventFlow for PageFetcherSnapshot: pageEventFlow for PageFetcherSnapshot: pageEventFlow for PageFetcherSnapshot: pageEventFlow for PageFetcherSnapshot: pageEventFlow for PageFetcherSnapshot
Combining the above clues, PagingData can be provided by triggering a refresh, which can be provided by calling the Refresh method of the PagingDataAdapter, and then layer down, It is finally triggered by calling refreshChannel.offer(true) in the Refresh method of the PagerUiReceiver object. .
Next comes the handling of page numbers. In pageEventFlow of PageFetcherSnapshot, you can see that when the refresh is triggered, LoadParams is built using the following methods and the load method of PagingSource is called to load the data.
// Some of the parameters are the original PagingConfig data, including the key, which is also the initialKey passed in when building Pager (default null)
private fun loadParams(loadType: LoadType, k! [](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/93c7b15b3994464ea550f091c8151101~tplv-k3u1fbpfcp-zoom-1.image)ey: Key?) = LoadParams.create(
loadType = loadType,
key = key,
loadSize = if (loadType == REFRESH) config.initialLoadSize else config.pageSize,
placeholdersEnabled = config.enablePlaceholders,
pageSize = config.pageSize
)
Copy the code
That is to say, when a page needs to jump, just trigger a refresh, and then load the data in the Load method of PagingSource using the desired page number, rather than the original page number brought in by LoadParams.
Pit point 2. Data will be reloaded when the interface is not visible for a period of time.
I don’t know if this is a pit, but it will affect the use experience of the APP (because the loading state is monitored, the loading circle will appear every time you switch back), so it must be checked.
The first thing that comes to mind is that the Refresh method for PageFetcher was called. One of the observation calls is the PagerUiReceiver object, which is specifically provided to the methods on the Adapter side. Then starting with the call point of PageFetcherSnapshot, after several debugging, it was found that the whole refresh behavior was because the flow of PageFetcher was rebuilt. After continuing to dig out, it was finally found that LiveData was the culprit. However, in my personal impression, even if the page is switched and the data in LiveData is not changed, the data will not be sent.
It is worth noting that the object Paging3 provides is the flow in the coroutine, and finally the LiveData object is passed through the asLiveData method.
@JvmOverloads
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}
Copy the code
An interesting argument, timeoutInMs, which defaults to a time when the interface is not visible, finally provides a CoroutineLiveData object.
Or because of the coroutine is not familiar with the reason, roughly understand the object CoroutineLiveData. If the interface is not visible, the coroutine will not be cancelled immediately. It will wait for the time of timeoutInMs to check whether the interface is visible. If the interface is not visible, the coroutine will be removed.
In summary, the reason for this problem is that Paging3 has a blocking coroutine, which stops blocking the execution of request data through refreshChannel and retryChannel in PageFetcher. After the interface switch is not visible, CoroutineLiveData cancels the coroutine and rebuilds it when the interface returns, triggering a refresh.
Solutions:
- Add cacheIn(viewModelScope) to PagingFlow.
- Do not use
CoroutineLiveData
, direct use lifecycleScope launchWhenCreated and flow to receive PagingData data.
conclusion
Paging3 is still being explored and used, and will be updated for larger problems encountered later.