More than technology, the article has material, concerned about the public number nine heart said, a high quality article every week, and nine heart in Dachang road side by side.
preface
In my previous article, I briefly discussed the usage of Paging 3. Some students immediately said that Paging 3 is a pagination library.
In fact, Paging 3 appears to be a Paging library, but in reality you only see the appearance. Removing the cloak of Paging, Paging can help us control the life cycle of data, from acquisition to display, providing an automated and standardized control process. Plus other components in Android Jetpack:
- Such as
LiveData
Can solve our component life cycle problems - Such as
Room
The provided PagingSource is refreshed in the UI as soon as new data is added to the database
Don’t ask, ask a word, sweet ~
As we all know, the best way to study source code is to study it with questions in mind.
So let’s start with Paging 3 and de-cloak it to see how it handles Paging:
- What does the whole process of paging look like?
- How do PagingSource and RemoteMediator work together?
- How to implement its state management?
directory
First, look at the structure from the perspective of use
The code used will not be described, but you can read Android Jetpack-Paging 3 to learn how to Use it.
From the previous article, we learned that there are several important classes:
class | instructions |
---|---|
PagingSource |
Data source for Paing. |
RemoteMediator |
If you have both local and remote data sources, this is where you can use themPagingSource Act as a local data source,RemoteMediator Act as a remote data source. |
Pager |
An entry point for data, which can be provided externallyFlow It is a responsive data source that can be retrieved by the receiverPagingData . |
PagingData |
We’re not going to get to that, but it’s worth knowing. The official note saysContainer for Paged data from a single generation of loads .single generation I understand that when the data source does not change, it is a generation, that is, there is no increase, deletion or change. |
RecyclerView |
Our old friend needs no introduction |
PagingDataAdapter |
The Paging to 3RecyclerView Tailor-made adapters |
From these classes, first establish a general structure diagram:
Notice that this PagingAdapter is connected to the Flow
Before reading this article, you should have some knowledge of coroutines. I recommend reading my article, Kotlin – Coroutines as You Learn.
Second, analysis preparation
Before moving on, I feel it is important to discuss the state management mechanism and event consumption mechanism in Paging 3.
1 Status management and event management
1.1 Status Management
Status management scenarios:
data class LoadStates(
/** [LoadState] corresponding to [LoadType.REFRESH] loads. */
val refresh: LoadState,
/** [LoadState] corresponding to [LoadType.PREPEND] loads. */
val prepend: LoadState,
/** [LoadState] corresponding to [LoadType.APPEND] loads. */
val append: LoadState
)
Copy the code
There are three status management scenarios:
refresh
: Refresh state, as the name impliesPager
The state of the initialization data.append
: The state of backloading, common in loading more scenarios.prepend
: A state in which data is loaded forward, usually starting at a point where data has not been loaded before.
So given these three scenarios, what are the states of it? The corresponding class for state is LoadState, which provides us with three states:
NotLoading
: not loaded, and not loaded is divided into load completed and load unfinished, by member variableendOfPaginationReached
Control.Loading
: in the loadError
Error:
With these states, we can do more things like interact with the UI and manage request events, for example.
1.2 Event Management
In addition to state management, we also need event management, so for example, if the data comes back, I need to notify an Insert event and include the state change, so event management actually includes state management.
There are also three types of event management:
internal sealed class PageEvent<T : Any> { // Intentional to prefer Refresh, Prepend, Append constructors from Companion. @Suppress("DataClassPrivateConstructor") data class Insert<T : Any> private constructor( val loadType: LoadType, val pages: List<TransformablePage<T>>, val placeholdersBefore: Int, val placeholdersAfter: Int, val combinedLoadStates: CombinedLoadStates ) : PageEvent<T>() { // ... } data class Drop<T : Any>( val loadType: LoadType, val minPageOffset: Int, val maxPageOffset: Int, val placeholdersRemaining: Int ) : PageEvent<T>() { // ... } data class LoadStateUpdate<T : Any>( val loadType: LoadType, val fromMediator: Boolean, val loadState: LoadState // TODO: consider using full state object here ) : PageEvent<T>() { //... }}Copy the code
Concrete is:
Insert
: Insert events, including specific datapages
Refresh\Append\Prepend; Refresh\Append\PrependloadType
And combined statecombinedLoadStates
(including Refresh\Append\Prepend loading state).Drop
: Delete eventLoadStateUpdate
: Events that load state changes.
Third, data generation
With that in mind, let’s go through the process.
The entry is when the PagingAdapter establishes a relationship with the Pager:
lifecycleScope.launch {
// Note: collectLatest is required. If you only use collect, the filter will not take effect
viewModel.shoes.collectLatest {
adapter.submitData(it)
}
}
Copy the code
Let’s talk about how the data is generated.
Viewmodel. shoes is a Flow provided by Pager:
// Build the Flow code when used
Pager(config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 20), pagingSourceFactory = { brand? .let { shoeRepository.getShoesByBrandPagingSource(it) } ? : shoeRepository.getAllShoesPagingSource() }).flowclass Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
config: PagingConfig,
initialKey: Key? = null.@OptIn(ExperimentalPagingApi::class)
remoteMediator: RemoteMediator<Key, Value>? = null,
pagingSourceFactory: () -> PagingSource<Key, Value>
) {
/** * A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become * invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or * [PagingDataAdapter.refresh]. */
val flow: Flow<PagingData<Value>> = PageFetcher(
pagingSourceFactory,
initialKey,
config,
remoteMediator
).flow
}
Copy the code
The first paragraph is the code in which we use Paging 3. Without further ado, it is listed just so you know that it has this process.
The second paragraph is the source code for Pager, which is really just a shell, and in the constructor, the last argument is a closure that returns a PagingSource. In addition, Pager provides a Flow of type Flow
1. PageFetcher
The Flow section of Pager looks like this:
internal class PageFetcher<Key : Any, Value : Any>(
/ /... Construction parameter omission
) {
// The Channel used to control the refresh
private val refreshChannel = ConflatedBroadcastChannel<Boolean> ()// Failed retry Channel
private val retryChannel = ConflatedBroadcastChannel<Unit> ()// The object built by paging builder can maintain the scope so that on rotation we don't stop
// the paging.
val flow: Flow<PagingData<Value>> = channelFlow {
Build RemoteMediatorAccessor
valremoteMediatorAccessor = remoteMediator? .let { RemoteMediatorAccessor(this, it)
}
// 2. Change refreshChannel to Flow
refreshChannel.asFlow()
.onStart {
// 3. The operation triggered before collect
@OptIn(ExperimentalPagingApi::class)emit(remoteMediatorAccessor? .initialize() == LAUNCH_INITIAL_REFRESH) } .scan(null) {
// 4. We can deal with the old result before calculating the new one
// ...
}
.filterNotNull()
.mapLatest { generation ->
// 5. Build PagingData by processing only the latest values
// ...
}
.collect { send(it) }
}
// ...
}
Copy the code
In view of the long, omitted a lot of code, the specific method to put the code.
1.1 Two Attributes
So let’s look at the two properties refreshChannel and retryChannel, which are actually used to send signals, refreshChannel is really important, it sends a refresh signal, and retryChannel sends a rerequest signal.
1.2 the outer
The returned Flow is wrapped in a layer of channelFlow, which is used either to transfer data across multiple coroutines or to have an indeterminate amount of data, which we’ll see later.
1.3 Building remote Data sources
Build a RemoteMediator Accessor that wraps the RemoteMediator for the remote data source.
Later, we will take each extension method of Flow as a part. When it comes to the specific extension method, we will not talk about its principle, but only its function. Interested students can have a look at its implementation.
1.4 create a Flow
RefreshChannel is converted to Flow, and the Flow#onStart method is called, which is called before the Flow can collect operation. What does this method do? Just one line of code:
remoteMediatorAccessor? .initialize() == LAUNCH_INITIAL_REFRESHCopy the code
RemoteMediator Accessor is a shell of RemoteMediator.
// The first call point
valremoteMediatorAccessor = remoteMediator? .let { RemoteMediatorAccessor(this, it)
}
/ / RemoteMediatorAccessor method
internal fun <Key : Any, Value : Any> RemoteMediatorAccessor(
scope: CoroutineScope,
delegate: RemoteMediator<Key, Value>
): RemoteMediatorAccessor<Key, Value> = RemoteMediatorAccessImpl(scope, delegate)
private class RemoteMediatorAccessImpl<Key : Any, Value : Any>(
private val scope: CoroutineScope,
private val remoteMediator: RemoteMediator<Key, Value>
) : RemoteMediatorAccessor<Key, Value> {
// ...
override suspend fun initialize(a): RemoteMediator.InitializeAction {
return remoteMediator.initialize().also { action ->
if (action == RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH) {
If RemoteMediator has default initialization behavior before collect, set the status
accessorState.use {
it.setBlockState(LoadType.APPEND, REQUIRES_REFRESH)
it.setBlockState(LoadType.PREPEND, REQUIRES_REFRESH)
}
}
}
}
// ...
}
Copy the code
From the code I’ve listed, RemoteMediatorAccessor wraps RemoteMediator, Remotemediatoraccessimply #initialize also calls the RemoteMediator#initialize method, which returns an enumeration InitializeAction of two types:
LAUNCH_INITIAL_REFRESH
: Emits a refresh signal during initializationSKIP_INITIAL_REFRESH
: does not emit a refresh signal during initialization, waiting for the UI request to send
Going back to the flow in PageFetcher, you can see that when you return to the onStart method, it has two cases:
- If you have
RemoteMediator
By default, it will launchtrue
. - There is no
RemoteMediator
Or initialize the remote data source by default without requesting itfalse
.
It can be interpreted as whether it wants to refresh the remote data source at initialization.
1.5 Scan
The function of this method is that every time the flow emits a new signal, you can get the new signal and calculate the new result. Before that, you can also get the old result for processing the old result.
As you can see from the parameters of this method:
previousGeneration
: The result of the last calculationtriggerRemoteRefresh
: Mentioned aboveonStart
Method, or called elsewhererefreshChannel
The emitted signal triggers a refresh of the remote data source.
internal class PageFetcher<Key : Any, Value : Any>(
private val pagingSourceFactory: () -> PagingSource<Key, Value>,
private valinitialKey: Key? .private val config: PagingConfig,
@OptIn(ExperimentalPagingApi::class)
private val remoteMediator: RemoteMediator<Key, Value>? = null
) {
// ...
// The object built by paging builder can maintain the scope so that on rotation we don't stop
// the paging.
val flow: Flow<PagingData<Value>> = channelFlow {
valremoteMediatorAccessor = remoteMediator? .let { RemoteMediatorAccessor(this, it)
}
refreshChannel.asFlow()
.onStart {
@OptIn(ExperimentalPagingApi::class)emit(remoteMediatorAccessor? .initialize() == LAUNCH_INITIAL_REFRESH) } .scan(null) { previousGeneration: PageFetcherSnapshot<Key, Value>? , triggerRemoteRefresh ->1. Generate a new data source
varpagingSource = generateNewPagingSource(previousGeneration? .pagingSource)while(pagingSource.invalid) { pagingSource = generateNewPagingSource(previousGeneration? .pagingSource) }@OptIn(ExperimentalPagingApi::class)
valinitialKey: Key? = previousGeneration? .refreshKeyInfo() ? .let { pagingSource.getRefreshKey(it) } ? : initialKey// 2. Release the old data sourcepreviousGeneration? .close()// 3. Generate new PageFetcherSnapshot
PageFetcherSnapshot<Key, Value>(
initialKey = initialKey,
pagingSource = pagingSource,
config = config,
retryFlow = retryChannel.asFlow(),
// Only trigger remote refresh on refresh signals that do not originate from
// initialization or PagingSource invalidation.
triggerRemoteRefresh = triggerRemoteRefresh,
remoteMediatorConnection = remoteMediatorAccessor,
invalidate = this@PageFetcher::refresh
)
}
.filterNotNull()
.mapLatest { generation ->
// ...
}
.collect { send(it) }
}
/ /...
}
Copy the code
This method does three things:
- Generate a new data source
PageSource
:PageFetcher#generateNewPagingSource
This method is calledPageFetcher
ConstructorpagingSourceFactory
A new data source is created and some listening is done. - Release old data sources.
- Return a new one
PageFetcherSnapshot
Object, which is the holding class for the data snapshot.
1.6 Filter null values
The Flow#filterNotNull method filters incoming empty values.
1.7 Processing the latest value
Flow#mapLatest only processes the latest value. While this method is working, a new value is sent upstream, and it stops working and processes the new value.
internal class PageFetcher<Key : Any, Value : Any>(
private val pagingSourceFactory: () -> PagingSource<Key, Value>,
private valinitialKey: Key? .private val config: PagingConfig,
@OptIn(ExperimentalPagingApi::class)
private val remoteMediator: RemoteMediator<Key, Value>? = null
) {
// ...
// The object built by paging builder can maintain the scope so that on rotation we don't stop
// the paging.
val flow: Flow<PagingData<Value>> = channelFlow {
valremoteMediatorAccessor = remoteMediator? .let { RemoteMediatorAccessor(this, it)
}
refreshChannel.asFlow()
.onStart {
@OptIn(ExperimentalPagingApi::class)emit(remoteMediatorAccessor? .initialize() == LAUNCH_INITIAL_REFRESH) } .scan(null) {
// ...
}
.filterNotNull()
.mapLatest { generation ->
val downstreamFlow = if (remoteMediatorAccessor == null) {
generation.pageEventFlow
} else {
generation.injectRemoteEvents(remoteMediatorAccessor)
}
PagingData(
flow = downstreamFlow,
receiver = PagerUiReceiver(generation, retryChannel)
)
}
.collect { send(it) }
}
/ /...
}
Copy the code
In the Flow#mapLatest method, it does two things:
- You get a stream of events
pageEventFlow
. - We’re going to encapsulate this stream of events
PagingData
.
1.8 send PagingData
Send the PagingData obtained above, which will eventually be consumed by the PagingDataAdapter, back to the code we wrote at the beginning:
// viewmodel. shoes is Flow
>
viewModel.shoes.collectLatest {
adapter.submitData(it)
}
Copy the code
To sum up, although the above process is many, in fact, the purpose is:
- get
PagingData
And thePagingData
The most important thing is the flow of eventsFlow<PageEvent<T>>
, it comes fromPageFetcherSnapshot
. - Depending on whether the code is enabled
RemoteMediator
.
2 PagingData
From 1, we learned that the event Flow
Boy, another big chunk of code, and the most important one is pageEventFlow:
internal class PageFetcherSnapshot<Key : Any, Value : Any>(
internal valinitialKey: Key? .internal val pagingSource: PagingSource<Key, Value>,
private val config: PagingConfig,
private val retryFlow: Flow<Unit>,
private val triggerRemoteRefresh: Boolean = false.val remoteMediatorConnection: RemoteMediatorConnection<Key, Value>? = null.private val invalidate: () -> Unit= {{})// ...
@OptIn(ExperimentalCoroutinesApi::class)
private val pageEventChCollected = AtomicBoolean(false)
private val pageEventCh = Channel<PageEvent<Value>>(Channel.BUFFERED)
private val stateLock = Mutex()
private val state = PageFetcherSnapshotState<Key, Value>(
config = config
)
private val pageEventChannelFlowJob = Job()
@OptIn(ExperimentalCoroutinesApi::class)
val pageEventFlow: Flow<PageEvent<Value>> = cancelableChannelFlow(pageEventChannelFlowJob) {
// 1. Create a coroutine pageEventCh to send the received event
launch {
pageEventCh.consumeAsFlow().collect {
// Protect against races where a subsequent call to submitData invoked close(),
// but a pageEvent arrives after closing causing ClosedSendChannelException.
try {
send(it)
} catch (e: ClosedSendChannelException) {
// Safe to drop PageEvent here, since collection has been cancelled.}}}// 2. Accept the retry information, is not for caching
val retryChannel = Channel<Unit>(Channel.RENDEZVOUS)
launch { retryFlow.collect { retryChannel.offer(it) } }
// 3. Retry action
launch {
retryChannel.consumeAsFlow()
.collect {
// Process the corresponding status after retry
// ...}}// 4. If you need to update remotely while refreshing, let remoteMediator load the data
if(triggerRemoteRefresh) { remoteMediatorConnection? .let {val pagingState = stateLock.withLock { state.currentPagingState(null) }
it.requestLoad(LoadType.REFRESH, pagingState)
}
}
// 5. PageSource initializes data
doInitialLoad(state)
// 6. Consumer hint
if (stateLock.withLock { state.sourceLoadStates.get(LoadType.REFRESH) } !is LoadState.Error) {
startConsumingHints()
}
}
// ...
}
Copy the code
PageEventFlow is divided into 6 parts. We focus on 1, 4, 5 and 6.
2.1 launch PageEvent
Create a coroutine to forward PageEvent
received by pageEventCh.
2.2 Requesting a Remote data source
If you create a remote data source and need to load remote data at initialization, start requesting remote data,
2.3 PagingSource initialization
There’s the first data initialization of the PagingSource, so what happens?
internal class PageFetcherSnapshot<Key : Any, Value : Any>(
// ...
) {
// ...
private suspend fun doInitialLoad(
state: PageFetcherSnapshotState<Key, Value>
) {
// 1. Set the current loading status - refresh
stateLock.withLock { state.setLoading(LoadType.REFRESH) }
// Build parameters
val params = loadParams(LoadType.REFRESH, initialKey)
// 2. Load data and get result
when (val result = pagingSource.load(params)) {
is PagingSource.LoadResult.Page<Key, Value> -> {
// 3
val insertApplied = stateLock.withLock { state.insert(0, LoadType.REFRESH, result) }
// 4. Handle the various states
stateLock.withLock {
state.setSourceLoadState(LoadType.REFRESH, LoadState.NotLoading.Incomplete)
if (result.prevKey == null) {
state.setSourceLoadState(
type = PREPEND,
newState = when (remoteMediatorConnection) {
null -> LoadState.NotLoading.Complete
else -> LoadState.NotLoading.Incomplete
}
)
}
if (result.nextKey == null) {
state.setSourceLoadState(
type = APPEND,
newState = when (remoteMediatorConnection) {
null -> LoadState.NotLoading.Complete
else -> LoadState.NotLoading.Incomplete
}
)
}
}
// 5. Send PageEvent
if (insertApplied) {
stateLock.withLock {
with(state) {
pageEventCh.send(result.toPageEvent(LoadType.REFRESH))
}
}
}
// 6. Whether the remote data request is necessary
if(remoteMediatorConnection ! =null) {
if (result.prevKey == null || result.nextKey == null) {
val pagingState =
stateLock.withLock { state.currentPagingState(lastHint) }
if (result.prevKey == null) {
remoteMediatorConnection.requestLoad(PREPEND, pagingState)
}
if (result.nextKey == null) {
remoteMediatorConnection.requestLoad(APPEND, pagingState)
}
}
}
}
is PagingSource.LoadResult.Error -> stateLock.withLock {
// Error status request
val loadState = LoadState.Error(result.throwable)
if (state.setSourceLoadState(LoadType.REFRESH, loadState)) {
pageEventCh.send(PageEvent.LoadStateUpdate(LoadType.REFRESH, false, loadState))
}
}
}
}
}
Copy the code
From the first time the data is initialized, you can see many things:
- Changes in data loading status: Refresh scenario
Incomplete
—Loading
– Set based on the returned resultComplete
和Incomplete
And some of the states will pass through the first partpageEventCh
Sends a status update event. - in
Refresh
The settingLoading
After the state, the loaded parameters are built into thepageSource
Make a data request and finally seepagingSource
. - because
pagingSource.load(params)
You can get one of two things, if it’s an error you just handle the error. - If it is a normal result, it will process the result first. Change the state again, and then launch one at a time
Insert
Events. - Because sometimes
pageSource
Failed to get result, set againremoteMediator
At this time, you need to use it againremoteMediator
Proceed to the next data request
This is the time to answer the first question:
If a remoteMediator exists, the data request will be made using the remoteMediator when the pageSource does not get the result.
2.4 How Can I Load More Data
If the first flush doesn’t go wrong, that is, the first flush doesn’t go wrong, the startConsumingHints method is called next:
internal class PageFetcherSnapshot<Key : Any, Value : Any>( // ... ) {/ /... private val state = PageFetcherSnapshotState<Key, Value>( config = config ) @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) private fun CoroutineScope.startConsumingHints() { // ... / / to monitor the Prepend news launch {state. ConsumePrependGenerationIdAsFlow () collectAsGenerationalViewportHints (the Prepend)} / / Listen to Append the message launch {state. ConsumeAppendGenerationIdAsFlow () collectAsGenerationalViewportHints (Append)}} private suspend fun Flow<Int>.collectAsGenerationalViewportHints( loadType: LoadType) = flatMapLatest {generationId -> statelock. withLock {//... } @OptIn(FlowPreview::class) hintChannel.asFlow() .drop(if (generationId == 0) 0 else 1) .map { hint -> GenerationalViewportHint(generationId, hint) } } .runningReduce { previous, next -> if (next.shouldPrioritizeOver(previous, LoadType)) next else previous}.conflate().collect {generationalHint -> Change status // 2. Request using PageSource // 3. 4. Determine whether to use RemoteMediator doLoad(state, loadType, generationalHint)}}Copy the code
The last part of doLoad is similar to the third part of doInitialLoad, with some differences. For example, when loading data, it determines whether the current data position is less than a threshold (set when PagerConfig is created) from the end or header of the loaded data. More will be loaded when this condition is true.
You might be a little dizzy here, but let’s use a picture to summarize what we talked about earlier:
After reading this section, it seems that there are still some questions that are not clear:
- How does the Ui drive the loading of more data than the initial part?
Fourth, data consumption
1. Specific consumption behavior
From the data returned upstream, we get PagingData
. Let’s see how the adapter PagingDataAdapter handles this data:
abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> @JvmOverloads constructor(
diffCallback: DiffUtil.ItemCallback<T>,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
workerDispatcher: CoroutineDispatcher = Dispatchers.Default
) : RecyclerView.Adapter<VH>() {
private val differ = AsyncPagingDataDiffer(
diffCallback = diffCallback,
updateCallback = AdapterListUpdateCallback(this),
mainDispatcher = mainDispatcher,
workerDispatcher = workerDispatcher
)
// ...
suspend fun submitData(pagingData: PagingData<T>) {
differ.submitData(pagingData)
}
// ...
}
Copy the code
PagingDataAdapter also doesn’t handle it itself, but hands it over to AsyncPagingDataDiffer, as the PagingDataAdapter annotation says, The PagingDataAdapter is just a shell for AsyncPagingDataDiffer, and the AsyncPagingDataDiffer is a hot potato for PagingDataDiffer.
abstract class PagingDataDiffer<T : Any>(
private val differCallback: DifferCallback,
private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main
) {
// ...
suspend fun collectFrom(pagingData: PagingData<T>) = collectFromRunner.runInIsolation {
// 1. Assign values to the current receiver
receiver = pagingData.receiver
pagingData.flow.collect { event ->
withContext<Unit>(mainDispatcher) {
// Switch to main thread
if (event is PageEvent.Insert && event.loadType == LoadType.REFRESH) {
// 1. The current is the insert event and the current is the refresh scenario
lastAccessedIndexUnfulfilled = false
// 2. Create a new PagePresenter that manages local data
val newPresenter = PagePresenter(event)
// 3. Recalculate the loading location of the data
val transformedLastAccessedIndex = presentNewList(
previousList = presenter,
newList = newPresenter,
newCombinedLoadStates = event.combinedLoadStates,
lastAccessedIndex = lastAccessedIndex
)
presenter = newPresenter
// Dispatch LoadState + DataRefresh updates as soon as we are done diffing,
// but after setting presenter.
dataRefreshedListeners.forEach { listener ->
listener(event.pages.all { page -> page.data.isEmpty() })
}
// 4. Notification status change
dispatchLoadStates(event.combinedLoadStates)
// 5. If the loading location of data changes, use receiver to send notificationtransformedLastAccessedIndex? .let { newIndex -> lastAccessedIndex = newIndex receiver? .accessHint( newPresenter.viewportHintForPresenterIndex(newIndex) ) } }else {
// ...
}
}
}
}
}
Copy the code
Using Refresh as an example, let’s take a quick look at how to consume data:
1.1 Cache data and notify UI of updates
In the case of Refresh, a data manager is built first, in this case PagePresenter.
Next comes notification data refresh, where the presentNewList method is handed to subclasses to implement:
private val differBase = object : PagingDataDiffer<T>(differCallback, mainDispatcher) { override suspend fun presentNewList( previousList: NullPaddedList<T>, newList: NullPaddedList<T>, newCombinedLoadStates: CombinedLoadStates, lastAccessedIndex: Int) = when {// fast path for no items -> some items previousList. Size == 0 -> {// Notification of new data insertion for the first time differCallback.onInserted(0, newList.size) null } // fast path for some items -> no items newList.size == 0 -> { differCallback.onRemoved(0, previousList.size) null } else -> { val diffResult = withContext(workerDispatcher) { previousList.computeDiff(newList, diffCallback) } previousList.dispatchDiff(updateCallback, newList, diffResult) previousList.transformAnchorIndex( diffResult = diffResult, newList = newList, oldPosition = lastAccessedIndex ) } }Copy the code
The first case is if there is data coming back from the first refresh. Use differCallback directly to notify that data has been added. Of course, this will notify our adapter PagingAdapter to call the corresponding method.
You might wonder a little bit here, but the adapter PagingAdapter doesn’t hold any data, so how does it get the data?
The PagingAdapter overwrites the getItem method, removing layers of nesting, and finally uses PagingDataDiffer:
abstract class PagingDataDiffer<T : Any>( private val differCallback: DifferCallback, private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main ) { // ... operator fun get(@IntRange(from = 0) index: Int): T? { lastAccessedIndexUnfulfilled = true lastAccessedIndex = index receiver? .accessHint(presenter.viewportHintForPresenterIndex(index)) return presenter.get(index) } }Copy the code
Therefore, the getItem method is implemented through the data manager PagePresenter. Otherwise, UiReceiver#accessHint is called every time the data is retrieved. When you still have data to load and the current display location is less than a certain threshold from the end of the data, the doLoad method is triggered, which causes Pager to load more data.
1.2 Notification of data status updates
Back in the PagingDataDiffer#collect method, after processing the above, the status is updated:
abstract class PagingDataDiffer<T : Any>(
private val differCallback: DifferCallback,
private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main
) {
// ...
private fun dispatchLoadStates(states: CombinedLoadStates) {
if (combinedLoadStates.snapshot() == states) return
combinedLoadStates.set(states)
loadStateListeners.forEach { it(states) }
}
}
Copy the code
So what can you do with these states? Can handle state with the user, for example, refresh error can switch wrong interface and so on.
1.3 Calculate whether the loading position of data changes
If it is not the first refresh and some data source has changed, such as deleting or adding data, some of the original location information is not accurate, you need to call the receiver. AccessHint method to send notifications.
The non-REFRESH case ends up doing something similar to Refresh without further elaboration.
To summarize the consumption process, use a picture to describe it:
The first and third questions we started with are clear:
The entire Paging 3 process is around the Flow
>, through which state and data changes are sent, the UI monitors data through it, and finally notifies the observers of the data and status.
conclusion
After reading the source code of Paging 3, I have two feelings:
- First point: the original coroutine can be used so amazing, the first time I feel that the coroutine I use is not the same thing as the big guy use coroutine, I cry ~
- Second point: message passing through the entire coroutine
Channel
In the realization of, combinedFlow
To write it, it’s really more concise and fluid.
In the next article, I will discuss how I used Paging 3 in my starting business.
Thanks for reading, and if you have another comment, feel free to leave a comment below!
If you think this article is good, it is the biggest encouragement for the author ~