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.lifecycle.Lifecycle 22 import androidx.paging.LoadState.NotLoading 23 import androidx.paging.LoadType.REFRESH 24 import androidx.recyclerview.widget.AdapterListUpdateCallback 25 import androidx.recyclerview.widget.ConcatAdapter 26 import androidx.recyclerview.widget.DiffUtil 27 import androidx.recyclerview.widget.RecyclerView 28 import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW 29 import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT 30 import kotlin.coroutines.CoroutineContext 31 import kotlinx.coroutines.CoroutineDispatcher 32 import kotlinx.coroutines.Dispatchers 33 import kotlinx.coroutines.flow.Flow 34 35 /** 36 * [RecyclerView.Adapter] base class for presenting paged data from [PagingData]s in a 37 * [RecyclerView]. 38 * 39 * This class is a convenience wrapper around [AsyncPagingDataDiffer] that implements common default 40 * behavior for item counting, and listening to update events. 41 * 42 * To present a [Pager], use [collectLatest][kotlinx.coroutines.flow.collectLatest] to observe 43 * [Pager.flow] and call [submitData] whenever a new [PagingData] is emitted. 44 * 45 * If using RxJava and LiveData extensions on [Pager], use the non-suspending overload of 46 * [submitData], which accepts a [Lifecycle]. 47 * 48 * [PagingDataAdapter] listens to internal [PagingData] loading events as 49 * [pages][PagingSource.LoadResult.Page] are loaded, and uses [DiffUtil] on a background thread to 50 * compute fine grained updates as updated content in the form of new PagingData objects are 51 * received. 52 * 53 * *State Restoration*: To be able to restore [RecyclerView] state (e.g. scroll position) after a 54 * configuration change / application recreate, [PagingDataAdapter] calls 55 * [RecyclerView.Adapter.setStateRestorationPolicy] with 56 * [RecyclerView.Adapter.StateRestorationPolicy.PREVENT] upon initialization and waits for the first 57 * page to load before allowing state restoration. Any other call to 58 * [RecyclerView.Adapter.setStateRestorationPolicy] by the application will disable this logic and 59 * will rely on the user set value. 60 * 61 * @sample androidx.paging.samples.pagingDataAdapterSample 62 */ 63 abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> 64 /** 65 * Construct a [PagingDataAdapter]. 66 * 67 * @param mainDispatcher [CoroutineContext] where UI events are dispatched. Typically, this should 68 * be [Dispatchers.Main]. 69 * @param workerDispatcher [CoroutineContext] where the work to generate UI events is dispatched, 70 * for example when diffing lists on [REFRESH]. Typically, this should have a background 71 * [CoroutineDispatcher] set; [Dispatchers.Default] by default. 72 * @param diffCallback Callback for calculating the diff between two non-disjoint lists on 73 * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster 74 * path for generating the UI events required to display the new list. 75 */ 76 @JvmOverloads 77 constructor( 78 diffCallback: DiffUtil.ItemCallback<T>, 79 mainDispatcher: CoroutineContext = Dispatchers.Main, 80 workerDispatcher: CoroutineContext = Dispatchers.Default, 81 ) : RecyclerView.Adapter<VH>() { 82 83 /** 84 * Construct a [PagingDataAdapter]. 85 * 86 * @param diffCallback Callback for calculating the diff between two non-disjoint lists on 87 * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster 88 * path for generating the UI events required to display the new list. 89 * @param mainDispatcher [CoroutineDispatcher] where UI events are dispatched. Typically, this 90 * should be [Dispatchers.Main]. 91 */ 92 @Deprecated( 93 message = "Superseded by constructors which accept CoroutineContext", 94 level = DeprecationLevel.HIDDEN 95 ) 96 // Only for binary compatibility; cannot apply @JvmOverloads as the function signature would 97 // conflict with the primary constructor. 98 @Suppress("MissingJvmstatic") 99 constructor( 100 diffCallback: DiffUtil.ItemCallback<T>, 101 mainDispatcher: CoroutineDispatcher = Dispatchers.Main, 102 ) : this( 103 diffCallback = diffCallback, 104 mainDispatcher = mainDispatcher, 105 workerDispatcher = Dispatchers.Default, 106 ) 107 108 /** 109 * Construct a [PagingDataAdapter]. 110 * 111 * @param diffCallback Callback for calculating the diff between two non-disjoint lists on 112 * [REFRESH]. Used as a fallback for item-level diffing when Paging is unable to find a faster 113 * path for generating the UI events required to display the new list. 114 * @param mainDispatcher [CoroutineDispatcher] where UI events are dispatched. Typically, this 115 * should be [Dispatchers.Main]. 116 * @param workerDispatcher [CoroutineDispatcher] where the work to generate UI events is 117 * dispatched, for example when diffing lists on [REFRESH]. Typically, this should dispatch on 118 * a background thread; [Dispatchers.Default] by default. 119 */ 120 @Deprecated( 121 message = "Superseded by constructors which accept CoroutineContext", 122 level = DeprecationLevel.HIDDEN 123 ) 124 // Only for binary compatibility; cannot apply @JvmOverloads as the function signature would 125 // conflict with the primary constructor. 126 @Suppress("MissingJvmstatic") 127 constructor( 128 diffCallback: DiffUtil.ItemCallback<T>, 129 mainDispatcher: CoroutineDispatcher = Dispatchers.Main, 130 workerDispatcher: CoroutineDispatcher = Dispatchers.Default, 131 ) : this( 132 diffCallback = diffCallback, 133 mainDispatcher = mainDispatcher, 134 workerDispatcher = workerDispatcher, 135 ) 136 137 /** 138 * Track whether developer called [setStateRestorationPolicy] or not to decide whether the 139 * automated state restoration should apply or not. 140 */ 141 private var userSetRestorationPolicy = false 142 143 override fun setStateRestorationPolicy(strategy: StateRestorationPolicy) { 144 userSetRestorationPolicy = true 145 super.setStateRestorationPolicy(strategy) 146 } 147 148 private val differ = 149 AsyncPagingDataDiffer( 150 diffCallback = diffCallback, 151 updateCallback = AdapterListUpdateCallback(this), 152 mainDispatcher = mainDispatcher, 153 workerDispatcher = workerDispatcher 154 ) 155 156 init { 157 // Wait on state restoration until the first insert event. 158 super.setStateRestorationPolicy(PREVENT) 159 160 fun considerAllowingStateRestoration() { 161 if (stateRestorationPolicy == PREVENT && !userSetRestorationPolicy) { 162 this@PagingDataAdapter.stateRestorationPolicy = ALLOW 163 } 164 } 165 166 // Watch for adapter insert before triggering state restoration. This is almost redundant 167 // with loadState below, but can handle cached case. 168 @Suppress("LeakingThis") 169 registerAdapterDataObserver( 170 object : RecyclerView.AdapterDataObserver() { 171 override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { 172 considerAllowingStateRestoration() 173 unregisterAdapterDataObserver(this) 174 super.onItemRangeInserted(positionStart, itemCount) 175 } 176 } 177 ) 178 179 // Watch for loadState update before triggering state restoration. This is almost 180 // redundant with data observer above, but can handle empty page case. 181 addLoadStateListener( 182 object : Function1<CombinedLoadStates, Unit> { 183 // Ignore the first event we get, which is always the initial state, since we only 184 // want to observe for Insert events. 185 private var ignoreNextEvent = true 186 187 override fun invoke(loadStates: CombinedLoadStates) { 188 if (ignoreNextEvent) { 189 ignoreNextEvent = false 190 } else if (loadStates.source.refresh is NotLoading) { 191 considerAllowingStateRestoration() 192 removeLoadStateListener(this) 193 } 194 } 195 } 196 ) 197 } 198 199 /** 200 * Note: [getItemId] is final, because stable IDs are unnecessary and therefore unsupported. 201 * 202 * [PagingDataAdapter]'s async diffing means that efficient change animations are handled for 203 * you, without the performance drawbacks of [RecyclerView.Adapter.notifyDataSetChanged]. 204 * Instead, the diffCallback parameter of the [PagingDataAdapter] serves the same 205 * functionality - informing the adapter and [RecyclerView] how items are changed and moved. 206 */ 207 final override fun getItemId(position: Int): Long { 208 return super.getItemId(position) 209 } 210 211 /** 212 * Stable ids are unsupported by [PagingDataAdapter]. Calling this method is an error and will 213 * result in an [UnsupportedOperationException]. 214 * 215 * @param hasStableIds Whether items in data set have unique identifiers or not. 216 * @throws UnsupportedOperationException Always thrown, since this is unsupported by 217 * [PagingDataAdapter]. 218 */ 219 final override fun setHasStableIds(hasStableIds: Boolean) { 220 throw UnsupportedOperationException("Stable ids are unsupported on PagingDataAdapter.") 221 } 222 223 /** 224 * Present a [PagingData] until it is invalidated by a call to [refresh] or 225 * [PagingSource.invalidate]. 226 * 227 * This method is typically used when collecting from a [Flow] produced by [Pager]. For RxJava 228 * or LiveData support, use the non-suspending overload of [submitData], which accepts a 229 * [Lifecycle]. 230 * 231 * Note: This method suspends while it is actively presenting page loads from a [PagingData], 232 * until the [PagingData] is invalidated. Although cancellation will propagate to this call 233 * automatically, collecting from a [Pager.flow] with the intention of presenting the most 234 * up-to-date representation of your backing dataset should typically be done using 235 * [collectLatest][kotlinx.coroutines.flow.collectLatest]. 236 * 237 * @sample androidx.paging.samples.submitDataFlowSample 238 * @see [Pager] 239 */ 240 suspend fun submitData(pagingData: PagingData<T>) { 241 differ.submitData(pagingData) 242 } 243 244 /** 245 * Present a [PagingData] until it is either invalidated or another call to [submitData] is 246 * made. 247 * 248 * This method is typically used when observing a RxJava or LiveData stream produced by [Pager]. 249 * For [Flow] support, use the suspending overload of [submitData], which automates cancellation 250 * via [CoroutineScope][kotlinx.coroutines.CoroutineScope] instead of relying of [Lifecycle]. 251 * 252 * @sample androidx.paging.samples.submitDataLiveDataSample 253 * @sample androidx.paging.samples.submitDataRxSample 254 * @see submitData 255 * @see [Pager] 256 */ 257 fun submitData(lifecycle: Lifecycle, pagingData: PagingData<T>) { 258 differ.submitData(lifecycle, pagingData) 259 } 260 261 /** 262 * Retry any failed load requests that would result in a [LoadState.Error] update to this 263 * [PagingDataAdapter]. 264 * 265 * Unlike [refresh], this does not invalidate [PagingSource], it only retries failed loads 266 * within the same generation of [PagingData]. 267 * 268 * [LoadState.Error] can be generated from two types of load requests: 269 * * [PagingSource.load] returning [PagingSource.LoadResult.Error] 270 * * [RemoteMediator.load] returning [RemoteMediator.MediatorResult.Error] 271 */ 272 fun retry() { 273 differ.retry() 274 } 275 276 /** 277 * Refresh the data presented by this [PagingDataAdapter]. 278 * 279 * [refresh] triggers the creation of a new [PagingData] with a new instance of [PagingSource] 280 * to represent an updated snapshot of the backing dataset. If a [RemoteMediator] is set, 281 * calling [refresh] will also trigger a call to [RemoteMediator.load] with [LoadType] [REFRESH] 282 * to allow [RemoteMediator] to check for updates to the dataset backing [PagingSource]. 283 * 284 * Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh. 285 * Invalidation due repository-layer signals, such as DB-updates, should instead use 286 * [PagingSource.invalidate]. 287 * 288 * @sample androidx.paging.samples.refreshSample 289 * @see PagingSource.invalidate 290 */ 291 fun refresh() { 292 differ.refresh() 293 } 294 295 /** 296 * Returns the presented item at the specified position, notifying Paging of the item access to 297 * trigger any loads necessary to fulfill [prefetchDistance][PagingConfig.prefetchDistance]. 298 * 299 * @param position Index of the presented item to return, including placeholders. 300 * @return The presented item at [position], `null` if it is a placeholder 301 */ 302 @MainThread protected fun getItem(@IntRange(from = 0) position: Int) = differ.getItem(position) 303 304 /** 305 * Returns the presented item at the specified position, without notifying Paging of the item 306 * access that would normally trigger page loads. 307 * 308 * @param index Index of the presented item to return, including placeholders. 309 * @return The presented item at position [index], `null` if it is a placeholder. 310 */ 311 @MainThread fun peek(@IntRange(from = 0) index: Int) = differ.peek(index) 312 313 /** 314 * Returns a new [ItemSnapshotList] representing the currently presented items, including any 315 * placeholders if they are enabled. 316 */ 317 fun snapshot(): ItemSnapshotList<T> = differ.snapshot() 318 319 override fun getItemCount() = differ.itemCount 320 321 /** 322 * A hot [Flow] of [CombinedLoadStates] that emits a snapshot whenever the loading state of the 323 * current [PagingData] changes. 324 * 325 * This flow is conflated, so it buffers the last update to [CombinedLoadStates] and immediately 326 * delivers the current load states on collection. 327 */ 328 val loadStateFlow: Flow<CombinedLoadStates> = differ.loadStateFlow 329 330 /** 331 * A hot [Flow] that emits after the pages presented to the UI are updated, even if the actual 332 * items presented don't change. 333 * 334 * An update is triggered from one of the following: 335 * * [submitData] is called and initial load completes, regardless of any differences in the 336 * loaded data 337 * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is inserted 338 * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is dropped 339 * 340 * Note: This is a [SharedFlow][kotlinx.coroutines.flow.SharedFlow] configured to replay 0 items 341 * with a buffer of size 64. If a collector lags behind page updates, it may trigger multiple 342 * times for each intermediate update that was presented while your collector was still working. 343 * To avoid this behavior, you can [conflate][kotlinx.coroutines.flow.conflate] this [Flow] so 344 * that you only receive the latest update, which is useful in cases where you are simply 345 * updating UI and don't care about tracking the exact number of page updates. 346 */ 347 val onPagesUpdatedFlow: Flow<Unit> = differ.onPagesUpdatedFlow 348 349 /** 350 * Add a [CombinedLoadStates] listener to observe the loading state of the current [PagingData]. 351 * 352 * As new [PagingData] generations are submitted and displayed, the listener will be notified to 353 * reflect the current [CombinedLoadStates]. 354 * 355 * @param listener [LoadStates] listener to receive updates. 356 * @sample androidx.paging.samples.addLoadStateListenerSample 357 * @see removeLoadStateListener 358 */ 359 fun addLoadStateListener(listener: (CombinedLoadStates) -> Unit) { 360 differ.addLoadStateListener(listener) 361 } 362 363 /** 364 * Remove a previously registered [CombinedLoadStates] listener. 365 * 366 * @param listener Previously registered listener. 367 * @see addLoadStateListener 368 */ 369 fun removeLoadStateListener(listener: (CombinedLoadStates) -> Unit) { 370 differ.removeLoadStateListener(listener) 371 } 372 373 /** 374 * Add a listener which triggers after the pages presented to the UI are updated, even if the 375 * actual items presented don't change. 376 * 377 * An update is triggered from one of the following: 378 * * [submitData] is called and initial load completes, regardless of any differences in the 379 * loaded data 380 * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is inserted 381 * * A [Page][androidx.paging.PagingSource.LoadResult.Page] is dropped 382 * 383 * @param listener called after pages presented are updated. 384 * @see removeOnPagesUpdatedListener 385 */ 386 fun addOnPagesUpdatedListener(listener: () -> Unit) { 387 differ.addOnPagesUpdatedListener(listener) 388 } 389 390 /** 391 * Remove a previously registered listener for new [PagingData] generations completing initial 392 * load and presenting to the UI. 393 * 394 * @param listener Previously registered listener. 395 * @see addOnPagesUpdatedListener 396 */ 397 fun removeOnPagesUpdatedListener(listener: () -> Unit) { 398 differ.removeOnPagesUpdatedListener(listener) 399 } 400 401 /** 402 * Create a [ConcatAdapter] with the provided [LoadStateAdapter]s displaying the 403 * [LoadType.PREPEND] [LoadState] as a list item at the end of the presented list. 404 * 405 * @see LoadStateAdapter 406 * @see withLoadStateHeaderAndFooter 407 * @see withLoadStateFooter 408 */ 409 fun withLoadStateHeader(header: LoadStateAdapter<*>): ConcatAdapter { 410 addLoadStateListener { loadStates -> header.loadState = loadStates.prepend } 411 return ConcatAdapter(header, this) 412 } 413 414 /** 415 * Create a [ConcatAdapter] with the provided [LoadStateAdapter]s displaying the 416 * [LoadType.APPEND] [LoadState] as a list item at the start of the presented list. 417 * 418 * @see LoadStateAdapter 419 * @see withLoadStateHeaderAndFooter 420 * @see withLoadStateHeader 421 */ 422 fun withLoadStateFooter(footer: LoadStateAdapter<*>): ConcatAdapter { 423 addLoadStateListener { loadStates -> footer.loadState = loadStates.append } 424 return ConcatAdapter(this, footer) 425 } 426 427 /** 428 * Create a [ConcatAdapter] with the provided [LoadStateAdapter]s displaying the 429 * [LoadType.PREPEND] and [LoadType.APPEND] [LoadState]s as list items at the start and end 430 * respectively. 431 * 432 * @see LoadStateAdapter 433 * @see withLoadStateHeader 434 * @see withLoadStateFooter 435 */ 436 fun withLoadStateHeaderAndFooter( 437 header: LoadStateAdapter<*>, 438 footer: LoadStateAdapter<*> 439 ): ConcatAdapter { 440 addLoadStateListener { loadStates -> 441 header.loadState = loadStates.prepend 442 footer.loadState = loadStates.append 443 } 444 return ConcatAdapter(header, this, footer) 445 } 446 } 447