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