1 /*
<lambda>null2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.paging
18 
19 import androidx.annotation.IntRange
20 import androidx.annotation.MainThread
21 import androidx.annotation.RestrictTo
22 import androidx.paging.LoadType.APPEND
23 import androidx.paging.LoadType.PREPEND
24 import androidx.paging.LoadType.REFRESH
25 import androidx.paging.PageEvent.Drop
26 import androidx.paging.PageEvent.Insert
27 import androidx.paging.PageEvent.StaticList
28 import androidx.paging.internal.CopyOnWriteArrayList
29 import androidx.paging.internal.appendMediatorStatesIfNotNull
30 import kotlin.concurrent.Volatile
31 import kotlin.coroutines.CoroutineContext
32 import kotlin.jvm.JvmSuppressWildcards
33 import kotlinx.coroutines.Dispatchers
34 import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
35 import kotlinx.coroutines.flow.Flow
36 import kotlinx.coroutines.flow.MutableSharedFlow
37 import kotlinx.coroutines.flow.MutableStateFlow
38 import kotlinx.coroutines.flow.StateFlow
39 import kotlinx.coroutines.flow.asSharedFlow
40 import kotlinx.coroutines.flow.update
41 import kotlinx.coroutines.withContext
42 import kotlinx.coroutines.yield
43 
44 /**
45  * The class that connects the UI layer to the underlying Paging operations. Takes input from UI
46  * presenters and outputs Paging events (Loads, LoadStateUpdate) in response.
47  *
48  * Paging front ends that implement this class will be able to access loaded data, LoadStates, and
49  * callbacks from LoadState or Page updates. This class also exposes the [PagingDataEvent] from a
50  * [PagingData] for custom logic on how to present Loads, Drops, and other Paging events.
51  *
52  * For implementation examples, refer to [AsyncPagingDataDiffer] for RecyclerView, or
53  * [LazyPagingItems] for Compose.
54  *
55  * @param [mainContext] The coroutine context that core Paging operations will run on. Defaults to
56  *   [Dispatchers.Main]. Main operations executed within this context include but are not limited
57  *   to:
58  * 1. flow collection on a [PagingData] for Loads, LoadStateUpdate etc.
59  * 2. emitting [CombinedLoadStates] to the [loadStateFlow]
60  * 3. invoking LoadState and PageUpdate listeners
61  * 4. invoking [presentPagingDataEvent]
62  *
63  * @param [cachedPagingData] a [PagingData] that will initialize this PagingDataPresenter with any
64  *   LoadStates or loaded data contained within it.
65  */
66 public abstract class PagingDataPresenter<T : Any>(
67     private val mainContext: CoroutineContext = Dispatchers.Main,
68     cachedPagingData: PagingData<T>? = null,
69 ) {
70     private var hintReceiver: HintReceiver? = null
71     private var uiReceiver: UiReceiver = InitialUiReceiver()
72     private var pageStore: PageStore<T> = PageStore.initial(cachedPagingData?.cachedEvent())
73     private val combinedLoadStatesCollection =
74         MutableCombinedLoadStateCollection().apply {
75             cachedPagingData?.cachedEvent()?.let { set(it.sourceLoadStates, it.mediatorLoadStates) }
76         }
77     private val onPagesUpdatedListeners = CopyOnWriteArrayList<() -> Unit>()
78 
79     private val collectFromRunner = SingleRunner()
80 
81     /**
82      * Track whether [lastAccessedIndex] points to a loaded item in the list or a placeholder after
83      * applying transformations to loaded pages. `true` if [lastAccessedIndex] points to a
84      * placeholder, `false` if [lastAccessedIndex] points to a loaded item after transformations.
85      *
86      * [lastAccessedIndexUnfulfilled] is used to track whether resending [lastAccessedIndex] as a
87      * hint is necessary, since in cases of aggressive filtering, an index may be unfulfilled after
88      * being sent to [PageFetcher], which is only capable of handling prefetchDistance before
89      * transformations.
90      */
91     @Volatile private var lastAccessedIndexUnfulfilled: Boolean = false
92 
93     /**
94      * Track last index access so it can be forwarded to new generations after DiffUtil runs and it
95      * is transformed to an index in the new list.
96      */
97     @Volatile private var lastAccessedIndex: Int = 0
98 
99     /**
100      * Handler for [PagingDataEvent] emitted by [PagingData].
101      *
102      * When a [PagingData] is submitted to this PagingDataPresenter through [collectFrom], page
103      * loads, drops, or LoadStateUpdates will be emitted to presenters as [PagingDataEvent] through
104      * this method.
105      *
106      * Presenter layers that communicate directly with [PagingDataPresenter] should override this
107      * method to handle the [PagingDataEvent] accordingly. For example by diffing two
108      * [PagingDataEvent.Refresh] lists, or appending the inserted list of data from
109      * [PagingDataEvent.Prepend] or [PagingDataEvent.Append].
110      */
111     public abstract suspend fun presentPagingDataEvent(
112         event: PagingDataEvent<T>,
113     ): @JvmSuppressWildcards Unit
114 
115     public suspend fun collectFrom(pagingData: PagingData<T>): @JvmSuppressWildcards Unit {
116         collectFromRunner.runInIsolation {
117             setUiReceiver(pagingData.uiReceiver)
118             pagingData.flow.collect { event ->
119                 log(VERBOSE) { "Collected $event" }
120                 withContext(mainContext) {
121                     /**
122                      * The hint receiver of a new generation is set only after it has been
123                      * presented. This ensures that:
124                      * 1. while new generation is still loading, access hints (and jump hints) will
125                      *    be sent to current generation.
126                      * 2. the access hint sent from presentNewList will have the correct
127                      *    placeholders and indexInPage adjusted according to new pageStore's most
128                      *    recent state
129                      *
130                      * Ensuring that viewport hints are sent to the correct generation helps
131                      * synchronize fetcher/pageStore in the correct calculation of the next
132                      * anchorPosition.
133                      */
134                     when {
135                         event is StaticList -> {
136                             presentNewList(
137                                 pages =
138                                     listOf(
139                                         TransformablePage(
140                                             originalPageOffset = 0,
141                                             data = event.data,
142                                         )
143                                     ),
144                                 placeholdersBefore = 0,
145                                 placeholdersAfter = 0,
146                                 dispatchLoadStates =
147                                     event.sourceLoadStates != null ||
148                                         event.mediatorLoadStates != null,
149                                 sourceLoadStates = event.sourceLoadStates,
150                                 mediatorLoadStates = event.mediatorLoadStates,
151                                 newHintReceiver = pagingData.hintReceiver
152                             )
153                         }
154                         event is Insert && (event.loadType == REFRESH) -> {
155                             presentNewList(
156                                 pages = event.pages,
157                                 placeholdersBefore = event.placeholdersBefore,
158                                 placeholdersAfter = event.placeholdersAfter,
159                                 dispatchLoadStates = true,
160                                 sourceLoadStates = event.sourceLoadStates,
161                                 mediatorLoadStates = event.mediatorLoadStates,
162                                 newHintReceiver = pagingData.hintReceiver
163                             )
164                         }
165                         event is Insert -> {
166                             if (inGetItem.value) {
167                                 yield()
168                             }
169                             // Process APPEND/PREPEND and send to presenter
170                             presentPagingDataEvent(pageStore.processEvent(event))
171 
172                             // dispatch load states
173                             combinedLoadStatesCollection.set(
174                                 sourceLoadStates = event.sourceLoadStates,
175                                 remoteLoadStates = event.mediatorLoadStates,
176                             )
177 
178                             // If index points to a placeholder after transformations, resend it
179                             // unless
180                             // there are no more items to load.
181                             val source = combinedLoadStatesCollection.stateFlow.value?.source
182                             checkNotNull(source) {
183                                 "PagingDataPresenter.combinedLoadStatesCollection.stateFlow " +
184                                     "should not hold null CombinedLoadStates after Insert event."
185                             }
186                             val prependDone = source.prepend.endOfPaginationReached
187                             val appendDone = source.append.endOfPaginationReached
188                             val canContinueLoading =
189                                 !(event.loadType == PREPEND && prependDone) &&
190                                     !(event.loadType == APPEND && appendDone)
191 
192                             /**
193                              * If the insert is empty due to aggressive filtering, another hint must
194                              * be sent to fetcher-side to notify that PagingDataPresenter received
195                              * the page, since fetcher estimates prefetchDistance based on page
196                              * indices presented by PagingDataPresenter and we cannot rely on a new
197                              * item being bound to trigger another hint since the presented page is
198                              * empty.
199                              */
200                             val emptyInsert = event.pages.all { it.data.isEmpty() }
201                             if (!canContinueLoading) {
202                                 // Reset lastAccessedIndexUnfulfilled since endOfPaginationReached
203                                 // means there are no more pages to load that could fulfill this
204                                 // index.
205                                 lastAccessedIndexUnfulfilled = false
206                             } else if (lastAccessedIndexUnfulfilled || emptyInsert) {
207                                 val shouldResendHint =
208                                     emptyInsert ||
209                                         lastAccessedIndex < pageStore.placeholdersBefore ||
210                                         lastAccessedIndex >
211                                             pageStore.placeholdersBefore + pageStore.dataCount
212 
213                                 if (shouldResendHint) {
214                                     hintReceiver?.accessHint(
215                                         pageStore.accessHintForPresenterIndex(lastAccessedIndex)
216                                     )
217                                 } else {
218                                     // lastIndex fulfilled, so reset lastAccessedIndexUnfulfilled.
219                                     lastAccessedIndexUnfulfilled = false
220                                 }
221                             }
222                         }
223                         event is Drop -> {
224                             if (inGetItem.value) {
225                                 yield()
226                             }
227                             // Process DROP and send to presenter
228                             presentPagingDataEvent(pageStore.processEvent(event))
229 
230                             // dispatch load states
231                             combinedLoadStatesCollection.set(
232                                 type = event.loadType,
233                                 remote = false,
234                                 state = LoadState.NotLoading.Incomplete
235                             )
236 
237                             // Reset lastAccessedIndexUnfulfilled if a page is dropped, to avoid
238                             // infinite loops when maxSize is insufficiently large.
239                             lastAccessedIndexUnfulfilled = false
240                         }
241                         event is PageEvent.LoadStateUpdate -> {
242                             combinedLoadStatesCollection.set(
243                                 sourceLoadStates = event.source,
244                                 remoteLoadStates = event.mediator,
245                             )
246                         }
247                     }
248                     // Notify page updates after pageStore processes them.
249                     //
250                     // Note: This is not redundant with LoadStates because it does not de-dupe
251                     // in cases where LoadState does not change, which would happen on cached
252                     // PagingData collections.
253                     if (event is Insert || event is Drop || event is StaticList) {
254                         onPagesUpdatedListeners.forEach { it() }
255                     }
256                 }
257             }
258         }
259     }
260 
261     private val inGetItem = MutableStateFlow(false)
262 
263     /**
264      * Returns the presented item at the specified position, notifying Paging of the item access to
265      * trigger any loads necessary to fulfill [prefetchDistance][PagingConfig.prefetchDistance].
266      *
267      * @param index Index of the presented item to return, including placeholders.
268      * @return The presented item at position [index], `null` if it is a placeholder.
269      */
270     @MainThread
271     public operator fun get(@IntRange(from = 0) index: Int): T? {
272         inGetItem.update { true }
273         lastAccessedIndexUnfulfilled = true
274         lastAccessedIndex = index
275 
276         log(VERBOSE) { "Accessing item index[$index]" }
277         hintReceiver?.accessHint(pageStore.accessHintForPresenterIndex(index))
278         return pageStore.get(index).also { inGetItem.update { false } }
279     }
280 
281     /**
282      * Returns the presented item at the specified position, without notifying Paging of the item
283      * access that would normally trigger page loads.
284      *
285      * @param index Index of the presented item to return, including placeholders.
286      * @return The presented item at position [index], `null` if it is a placeholder
287      */
288     @MainThread
289     public fun peek(@IntRange(from = 0) index: Int): T? {
290         return pageStore.get(index)
291     }
292 
293     /**
294      * Returns a new [ItemSnapshotList] representing the currently presented items, including any
295      * placeholders if they are enabled.
296      */
297     public fun snapshot(): ItemSnapshotList<T> = pageStore.snapshot()
298 
299     /**
300      * Retry any failed load requests that would result in a [LoadState.Error] update to this
301      * [PagingDataPresenter].
302      *
303      * Unlike [refresh], this does not invalidate [PagingSource], it only retries failed loads
304      * within the same generation of [PagingData].
305      *
306      * [LoadState.Error] can be generated from two types of load requests:
307      * * [PagingSource.load] returning [PagingSource.LoadResult.Error]
308      * * [RemoteMediator.load] returning [RemoteMediator.MediatorResult.Error]
309      */
310     public fun retry() {
311         log(DEBUG) { "Retry signal received" }
312         uiReceiver.retry()
313     }
314 
315     /**
316      * Refresh the data presented by this [PagingDataPresenter].
317      *
318      * [refresh] triggers the creation of a new [PagingData] with a new instance of [PagingSource]
319      * to represent an updated snapshot of the backing dataset. If a [RemoteMediator] is set,
320      * calling [refresh] will also trigger a call to [RemoteMediator.load] with [LoadType] [REFRESH]
321      * to allow [RemoteMediator] to check for updates to the dataset backing [PagingSource].
322      *
323      * Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh.
324      * Invalidation due repository-layer signals, such as DB-updates, should instead use
325      * [PagingSource.invalidate].
326      *
327      * @sample androidx.paging.samples.refreshSample
328      * @see PagingSource.invalidate
329      */
330     public fun refresh() {
331         log(DEBUG) { "Refresh signal received" }
332         uiReceiver.refresh()
333     }
334 
335     /** @return Total number of presented items, including placeholders. */
336     public val size: Int
337         get() = pageStore.size
338 
339     /**
340      * A hot [Flow] of [CombinedLoadStates] that emits a snapshot whenever the loading state of the
341      * current [PagingData] changes.
342      *
343      * This flow is conflated. It buffers the last update to [CombinedLoadStates] and immediately
344      * delivers the current load states on collection, unless this [PagingDataPresenter] has not
345      * been hooked up to a [PagingData] yet, and thus has no state to emit.
346      *
347      * @sample androidx.paging.samples.loadStateFlowSample
348      */
349     public val loadStateFlow: StateFlow<CombinedLoadStates?> =
350         combinedLoadStatesCollection.stateFlow
351 
352     private val _onPagesUpdatedFlow: MutableSharedFlow<Unit> =
353         MutableSharedFlow(
354             replay = 0,
355             extraBufferCapacity = 64,
356             onBufferOverflow = DROP_OLDEST,
357         )
358 
359     /**
360      * A hot [Flow] that emits after the pages presented to the UI are updated, even if the actual
361      * items presented don't change.
362      *
363      * An update is triggered from one of the following:
364      * * [collectFrom] is called and initial load completes, regardless of any differences in the
365      *   loaded data
366      * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is inserted
367      * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is dropped
368      *
369      * Note: This is a [SharedFlow][kotlinx.coroutines.flow.SharedFlow] configured to replay 0 items
370      * with a buffer of size 64. If a collector lags behind page updates, it may trigger multiple
371      * times for each intermediate update that was presented while your collector was still working.
372      * To avoid this behavior, you can [conflate][kotlinx.coroutines.flow.conflate] this [Flow] so
373      * that you only receive the latest update, which is useful in cases where you are simply
374      * updating UI and don't care about tracking the exact number of page updates.
375      */
376     public val onPagesUpdatedFlow: Flow<Unit>
377         get() = _onPagesUpdatedFlow.asSharedFlow()
378 
379     init {
380         addOnPagesUpdatedListener { _onPagesUpdatedFlow.tryEmit(Unit) }
381     }
382 
383     /**
384      * Add a listener which triggers after the pages presented to the UI are updated, even if the
385      * actual items presented don't change.
386      *
387      * An update is triggered from one of the following:
388      * * [collectFrom] is called and initial load completes, regardless of any differences in the
389      *   loaded data
390      * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is inserted
391      * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is dropped
392      *
393      * @param listener called after pages presented are updated.
394      * @see removeOnPagesUpdatedListener
395      */
396     public fun addOnPagesUpdatedListener(listener: () -> Unit) {
397         onPagesUpdatedListeners.add(listener)
398     }
399 
400     /**
401      * Remove a previously registered listener for updates to presented pages.
402      *
403      * @param listener Previously registered listener.
404      * @see addOnPagesUpdatedListener
405      */
406     public fun removeOnPagesUpdatedListener(listener: () -> Unit) {
407         onPagesUpdatedListeners.remove(listener)
408     }
409 
410     /**
411      * Add a [CombinedLoadStates] listener to observe the loading state of the current [PagingData].
412      *
413      * As new [PagingData] generations are submitted and displayed, the listener will be notified to
414      * reflect the current [CombinedLoadStates].
415      *
416      * When a new listener is added, it will be immediately called with the current
417      * [CombinedLoadStates], unless this [PagingDataPresenter] has not been hooked up to a
418      * [PagingData] yet, and thus has no state to emit.
419      *
420      * @param listener [LoadStates] listener to receive updates.
421      * @sample androidx.paging.samples.addLoadStateListenerSample
422      * @see removeLoadStateListener
423      */
424     public fun addLoadStateListener(listener: (@JvmSuppressWildcards CombinedLoadStates) -> Unit) {
425         combinedLoadStatesCollection.addListener(listener)
426     }
427 
428     /**
429      * Remove a previously registered [CombinedLoadStates] listener.
430      *
431      * @param listener Previously registered listener.
432      * @see addLoadStateListener
433      */
434     public fun removeLoadStateListener(
435         listener: (@JvmSuppressWildcards CombinedLoadStates) -> Unit
436     ) {
437         combinedLoadStatesCollection.removeListener(listener)
438     }
439 
440     private suspend fun presentNewList(
441         pages: List<TransformablePage<T>>,
442         placeholdersBefore: Int,
443         placeholdersAfter: Int,
444         dispatchLoadStates: Boolean,
445         sourceLoadStates: LoadStates?,
446         mediatorLoadStates: LoadStates?,
447         newHintReceiver: HintReceiver,
448     ) {
449         require(!dispatchLoadStates || sourceLoadStates != null) {
450             "Cannot dispatch LoadStates in PagingDataPresenter without source LoadStates set."
451         }
452 
453         lastAccessedIndexUnfulfilled = false
454 
455         val newPageStore =
456             PageStore(
457                 pages = pages,
458                 placeholdersBefore = placeholdersBefore,
459                 placeholdersAfter = placeholdersAfter,
460             )
461         // must capture previousList states here before we update pageStore
462         val previousList = pageStore as PlaceholderPaddedList<T>
463 
464         // update the store here before event is sent to ensure that snapshot() returned in
465         // UI update callbacks (onChanged, onInsert etc) reflects the new list
466         pageStore = newPageStore
467         hintReceiver = newHintReceiver
468 
469         // send event to UI
470         presentPagingDataEvent(
471             PagingDataEvent.Refresh(
472                 newList = newPageStore as PlaceholderPaddedList<T>,
473                 previousList = previousList,
474             )
475         )
476         log(DEBUG) {
477             appendMediatorStatesIfNotNull(mediatorLoadStates) {
478                 """Presenting data (
479                             |   first item: ${pages.firstOrNull()?.data?.firstOrNull()}
480                             |   last item: ${pages.lastOrNull()?.data?.lastOrNull()}
481                             |   placeholdersBefore: $placeholdersBefore
482                             |   placeholdersAfter: $placeholdersAfter
483                             |   hintReceiver: $newHintReceiver
484                             |   sourceLoadStates: $sourceLoadStates
485                         """
486             }
487         }
488         // We may want to skip dispatching load states if triggered by a static list which wants to
489         // preserve the previous state.
490         if (dispatchLoadStates) {
491             // Dispatch LoadState updates as soon as we are done diffing, but after
492             // setting new pageStore.
493             combinedLoadStatesCollection.set(sourceLoadStates!!, mediatorLoadStates)
494         }
495         if (newPageStore.size == 0) {
496             // Send an initialize hint in case the new list is empty (no items or placeholders),
497             // which would prevent a ViewportHint.Access from ever getting sent since there are
498             // no items to bind from initial load. Without this hint, paging would stall on
499             // an empty list because prepend/append would be not triggered.
500             hintReceiver?.accessHint(newPageStore.initializeHint())
501         }
502     }
503 
504     // Holds on to retry/refresh requests to deliver them when the real UiReceiver is attached.
505     private class InitialUiReceiver : UiReceiver {
506         var retry = false
507         var refresh = false
508 
509         override fun retry() {
510             retry = true
511         }
512 
513         override fun refresh() {
514             refresh = true
515         }
516     }
517 
518     private fun setUiReceiver(receiver: UiReceiver) {
519         val oldReceiver = this.uiReceiver
520         this.uiReceiver = receiver
521         if (oldReceiver is InitialUiReceiver) {
522             if (oldReceiver.retry) {
523                 receiver.retry()
524             }
525             if (oldReceiver.refresh) {
526                 receiver.refresh()
527             }
528         }
529     }
530 }
531 
532 /**
533  * Payloads used to dispatch change events. Could become a public API post 3.0 in case developers
534  * want to handle it more effectively.
535  *
536  * Sending these change payloads is critical for the common case where DefaultItemAnimator won't
537  * animate them and re-use the same view holder if possible.
538  */
539 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
540 public enum class DiffingChangePayload {
541     ITEM_TO_PLACEHOLDER,
542     PLACEHOLDER_TO_ITEM,
543     PLACEHOLDER_POSITION_CHANGE
544 }
545