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