1 /*
2  * 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.VisibleForTesting
21 import androidx.paging.LoadType.REFRESH
22 
23 /**
24  * Base class for an abstraction of pageable static data from some source, where loading pages of
25  * data is typically an expensive operation. Some examples of common [PagingSource]s might be from
26  * network or from a database.
27  *
28  * An instance of a [PagingSource] is used to load pages of data for an instance of [PagingData].
29  *
30  * A [PagingData] can grow as it loads more data, but the data loaded cannot be updated. If the
31  * underlying data set is modified, a new [PagingSource] / [PagingData] pair must be created to
32  * represent an updated snapshot of the data.
33  *
34  * ### Loading Pages
35  *
36  * [PagingData] queries data from its [PagingSource] in response to loading hints generated as the
37  * user scrolls in a `RecyclerView`.
38  *
39  * To control how and when a [PagingData] queries data from its [PagingSource], see [PagingConfig],
40  * which defines behavior such as [PagingConfig.pageSize] and [PagingConfig.prefetchDistance].
41  *
42  * ### Updating Data
43  *
44  * A [PagingSource] / [PagingData] pair is a snapshot of the data set. A new [PagingData] /
45  * [PagingData] must be created if an update occurs, such as a reorder, insert, delete, or content
46  * update occurs. A [PagingSource] must detect that it cannot continue loading its snapshot (for
47  * instance, when Database query notices a table being invalidated), and call [invalidate]. Then a
48  * new [PagingSource] / [PagingData] pair would be created to represent data from the new state of
49  * the database query.
50  *
51  * ### Presenting Data to UI
52  *
53  * To present data loaded by a [PagingSource] to a `RecyclerView`, create an instance of [Pager],
54  * which provides a stream of [PagingData] that you may collect from and submit to a
55  * [PagingDataAdapter][androidx.paging.PagingDataAdapter].
56  *
57  * @param Key Type of key which define what data to load. E.g. [Int] to represent either a page
58  *   number or item position, or [String] if your network uses Strings as next tokens returned with
59  *   each response.
60  * @param Value Type of data loaded in by this [PagingSource]. E.g., the type of data that will be
61  *   passed to a [PagingDataAdapter][androidx.paging.PagingDataAdapter] to be displayed in a
62  *   `RecyclerView`.
63  * @sample androidx.paging.samples.pageKeyedPagingSourceSample
64  * @sample androidx.paging.samples.itemKeyedPagingSourceSample
65  * @see Pager
66  */
67 public abstract class PagingSource<Key : Any, Value : Any> {
68 
69     private val invalidateCallbackTracker =
<lambda>null70         InvalidateCallbackTracker<() -> Unit>(callbackInvoker = { it() })
71 
72     internal val invalidateCallbackCount: Int
73         @VisibleForTesting get() = invalidateCallbackTracker.callbackCount()
74 
75     /** Params for a load request on a [PagingSource] from [PagingSource.load]. */
76     public sealed class LoadParams<Key : Any>
77     constructor(
78         /**
79          * Requested number of items to load.
80          *
81          * Note: It is valid for [PagingSource.load] to return a [LoadResult] that has a different
82          * number of items than the requested load size.
83          */
84         public val loadSize: Int,
85         /**
86          * From [PagingConfig.enablePlaceholders], true if placeholders are enabled and the load
87          * request for this [LoadParams] should populate [LoadResult.Page.itemsBefore] and
88          * [LoadResult.Page.itemsAfter] if possible.
89          */
90         public val placeholdersEnabled: Boolean,
91     ) {
92         /**
93          * Key for the page to be loaded.
94          *
95          * [key] can be `null` only if this [LoadParams] is [Refresh], and either no `initialKey` is
96          * provided to the [Pager] or [PagingSource.getRefreshKey] from the previous [PagingSource]
97          * returns `null`.
98          *
99          * The value of [key] is dependent on the type of [LoadParams]:
100          * * [Refresh]
101          *     * On initial load, the nullable `initialKey` passed to the [Pager].
102          *     * On subsequent loads due to invalidation or refresh, the result of
103          *       [PagingSource.getRefreshKey].
104          * * [Prepend] - [LoadResult.Page.prevKey] of the loaded page at the front of the list.
105          * * [Append] - [LoadResult.Page.nextKey] of the loaded page at the end of the list.
106          */
107         public abstract val key: Key?
108 
109         /**
110          * Params for an initial load request on a [PagingSource] from [PagingSource.load] or a
111          * refresh triggered by [invalidate].
112          */
113         public class Refresh<Key : Any>
114         constructor(
115             override val key: Key?,
116             loadSize: Int,
117             placeholdersEnabled: Boolean,
118         ) :
119             LoadParams<Key>(
120                 loadSize = loadSize,
121                 placeholdersEnabled = placeholdersEnabled,
122             )
123 
124         /**
125          * Params to load a page of data from a [PagingSource] via [PagingSource.load] to be
126          * appended to the end of the list.
127          */
128         public class Append<Key : Any>
129         constructor(
130             override val key: Key,
131             loadSize: Int,
132             placeholdersEnabled: Boolean,
133         ) :
134             LoadParams<Key>(
135                 loadSize = loadSize,
136                 placeholdersEnabled = placeholdersEnabled,
137             )
138 
139         /**
140          * Params to load a page of data from a [PagingSource] via [PagingSource.load] to be
141          * prepended to the start of the list.
142          */
143         public class Prepend<Key : Any>
144         constructor(
145             override val key: Key,
146             loadSize: Int,
147             placeholdersEnabled: Boolean,
148         ) :
149             LoadParams<Key>(
150                 loadSize = loadSize,
151                 placeholdersEnabled = placeholdersEnabled,
152             )
153 
154         internal companion object {
createnull155             fun <Key : Any> create(
156                 loadType: LoadType,
157                 key: Key?,
158                 loadSize: Int,
159                 placeholdersEnabled: Boolean,
160             ): LoadParams<Key> =
161                 when (loadType) {
162                     LoadType.REFRESH ->
163                         Refresh(
164                             key = key,
165                             loadSize = loadSize,
166                             placeholdersEnabled = placeholdersEnabled,
167                         )
168                     LoadType.PREPEND ->
169                         Prepend(
170                             loadSize = loadSize,
171                             key = requireNotNull(key) { "key cannot be null for prepend" },
172                             placeholdersEnabled = placeholdersEnabled,
173                         )
174                     LoadType.APPEND ->
175                         Append(
176                             loadSize = loadSize,
177                             key = requireNotNull(key) { "key cannot be null for append" },
178                             placeholdersEnabled = placeholdersEnabled,
179                         )
180                 }
181         }
182     }
183 
184     /** Result of a load request from [PagingSource.load]. */
185     public sealed class LoadResult<Key : Any, Value : Any> {
186         /**
187          * Error result object for [PagingSource.load].
188          *
189          * This return type indicates an expected, recoverable error (such as a network load
190          * failure). This failure will be forwarded to the UI as a [LoadState.Error], and may be
191          * retried.
192          *
193          * @sample androidx.paging.samples.pageKeyedPagingSourceSample
194          */
195         @Suppress("DataClassDefinition")
196         public data class Error<Key : Any, Value : Any>(val throwable: Throwable) :
197             LoadResult<Key, Value>() {
toStringnull198             override fun toString(): String {
199                 return """LoadResult.Error(
200                     |   throwable: $throwable
201                     |) """
202                     .trimMargin()
203             }
204         }
205 
206         /**
207          * Invalid result object for [PagingSource.load]
208          *
209          * This return type can be used to terminate future load requests on this [PagingSource]
210          * when the [PagingSource] is not longer valid due to changes in the underlying dataset.
211          *
212          * For example, if the underlying database gets written into but the [PagingSource] does not
213          * invalidate in time, it may return inconsistent results if its implementation depends on
214          * the immutability of the backing dataset it loads from (e.g., LIMIT OFFSET style db
215          * implementations). In this scenario, it is recommended to check for invalidation after
216          * loading and to return LoadResult.Invalid, which causes Paging to discard any pending or
217          * future load requests to this PagingSource and invalidate it.
218          *
219          * Returning [Invalid] will trigger Paging to [invalidate] this [PagingSource] and terminate
220          * any future attempts to [load] from this [PagingSource]
221          */
222         public class Invalid<Key : Any, Value : Any> : LoadResult<Key, Value>() {
toStringnull223             override fun toString(): String {
224                 return "LoadResult.Invalid"
225             }
226         }
227 
228         /**
229          * Success result object for [PagingSource.load].
230          *
231          * As a convenience, iterating on this object will iterate through its loaded [data].
232          *
233          * @sample androidx.paging.samples.pageKeyedPage
234          * @sample androidx.paging.samples.pageIndexedPage
235          */
236         @Suppress("DataClassDefinition")
237         public data class Page<Key : Any, Value : Any>
238         constructor(
239             /** Loaded data */
240             val data: List<Value>,
241             /**
242              * [Key] for previous page if more data can be loaded in that direction, `null`
243              * otherwise.
244              */
245             val prevKey: Key?,
246             /**
247              * [Key] for next page if more data can be loaded in that direction, `null` otherwise.
248              */
249             val nextKey: Key?,
250             /**
251              * Count of items before the loaded data. Must be implemented if
252              * [jumping][PagingSource.jumpingSupported] is enabled. Optional otherwise.
253              */
254             @IntRange(from = COUNT_UNDEFINED.toLong()) val itemsBefore: Int = COUNT_UNDEFINED,
255             /**
256              * Count of items after the loaded data. Must be implemented if
257              * [jumping][PagingSource.jumpingSupported] is enabled. Optional otherwise.
258              */
259             @IntRange(from = COUNT_UNDEFINED.toLong()) val itemsAfter: Int = COUNT_UNDEFINED
260         ) : LoadResult<Key, Value>(), Iterable<Value> {
261 
262             /**
263              * Success result object for [PagingSource.load].
264              *
265              * @param data Loaded data
266              * @param prevKey [Key] for previous page if more data can be loaded in that direction,
267              *   `null` otherwise.
268              * @param nextKey [Key] for next page if more data can be loaded in that direction,
269              *   `null` otherwise.
270              */
271             public constructor(
272                 data: List<Value>,
273                 prevKey: Key?,
274                 nextKey: Key?
275             ) : this(data, prevKey, nextKey, COUNT_UNDEFINED, COUNT_UNDEFINED)
276 
277             init {
<lambda>null278                 require(itemsBefore == COUNT_UNDEFINED || itemsBefore >= 0) {
279                     "itemsBefore cannot be negative"
280                 }
281 
<lambda>null282                 require(itemsAfter == COUNT_UNDEFINED || itemsAfter >= 0) {
283                     "itemsAfter cannot be negative"
284                 }
285             }
286 
iteratornull287             override fun iterator(): Iterator<Value> {
288                 return data.listIterator()
289             }
290 
toStringnull291             override fun toString(): String {
292                 return """LoadResult.Page(
293                     |   data size: ${data.size}
294                     |   first Item: ${data.firstOrNull()}
295                     |   last Item: ${data.lastOrNull()}
296                     |   nextKey: $nextKey
297                     |   prevKey: $prevKey
298                     |   itemsBefore: $itemsBefore
299                     |   itemsAfter: $itemsAfter
300                     |) """
301                     .trimMargin()
302             }
303 
304             public companion object {
305                 public const val COUNT_UNDEFINED: Int = Int.MIN_VALUE
306 
307                 @Suppress("MemberVisibilityCanBePrivate") // Prevent synthetic accessor generation.
308                 internal val EMPTY = Page(emptyList(), null, null, 0, 0)
309 
310                 @Suppress("UNCHECKED_CAST") // Can safely ignore, since the list is empty.
emptynull311                 internal fun <Key : Any, Value : Any> empty() = EMPTY as Page<Key, Value>
312             }
313         }
314     }
315 
316     /**
317      * `true` if this [PagingSource] supports jumping, `false` otherwise.
318      *
319      * Override this to `true` if pseudo-fast scrolling via jumps is supported.
320      *
321      * A jump occurs when a `RecyclerView` scrolls through a number of placeholders defined by
322      * [PagingConfig.jumpThreshold] and triggers a load with [LoadType] [REFRESH].
323      *
324      * [PagingSource]s that support jumps should override [getRefreshKey] to return a [Key] that
325      * would load data fulfilling the viewport given a user's current [PagingState.anchorPosition].
326      *
327      * To support jumping, the [LoadResult.Page] returned from this PagingSource must implement
328      * [itemsBefore][LoadResult.Page.itemsBefore] and [itemsAfter][LoadResult.Page.itemsAfter] to
329      * notify Paging the boundaries within which it can jump.
330      *
331      * @see [PagingConfig.jumpThreshold]
332      */
333     public open val jumpingSupported: Boolean
334         get() = false
335 
336     /**
337      * `true` if this [PagingSource] expects to re-use keys to load distinct pages without a call to
338      * [invalidate], `false` otherwise.
339      */
340     public open val keyReuseSupported: Boolean
341         get() = false
342 
343     /**
344      * Whether this [PagingSource] has been invalidated, which should happen when the data this
345      * [PagingSource] represents changes since it was first instantiated.
346      */
347     public val invalid: Boolean
348         get() = invalidateCallbackTracker.invalid
349 
350     /**
351      * Signal the [PagingSource] to stop loading.
352      *
353      * This method is idempotent. i.e., If [invalidate] has already been called, subsequent calls to
354      * this method should have no effect.
355      */
356     public fun invalidate() {
357         if (invalidateCallbackTracker.invalidate()) {
358             log(DEBUG) { "Invalidated PagingSource $this" }
359         }
360     }
361 
362     /**
363      * Add a callback to invoke when the [PagingSource] is first invalidated.
364      *
365      * Once invalidated, a [PagingSource] will not become valid again.
366      *
367      * A [PagingSource] will only invoke its callbacks once - the first time [invalidate] is called,
368      * on that thread.
369      *
370      * If this [PagingSource] is already invalid, the provided [onInvalidatedCallback] will be
371      * triggered immediately.
372      *
373      * @param onInvalidatedCallback The callback that will be invoked on thread that invalidates the
374      *   [PagingSource].
375      */
registerInvalidatedCallbacknull376     public fun registerInvalidatedCallback(onInvalidatedCallback: () -> Unit) {
377         invalidateCallbackTracker.registerInvalidatedCallback(onInvalidatedCallback)
378     }
379 
380     /**
381      * Remove a previously added invalidate callback.
382      *
383      * @param onInvalidatedCallback The previously added callback.
384      */
unregisterInvalidatedCallbacknull385     public fun unregisterInvalidatedCallback(onInvalidatedCallback: () -> Unit) {
386         invalidateCallbackTracker.unregisterInvalidatedCallback(onInvalidatedCallback)
387     }
388 
389     /**
390      * Loading API for [PagingSource].
391      *
392      * Implement this method to trigger your async load (e.g. from database or network).
393      */
loadnull394     public abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
395 
396     /**
397      * Provide a [Key] used for the initial [load] for the next [PagingSource] due to invalidation
398      * of this [PagingSource]. The [Key] is provided to [load] via [LoadParams.key].
399      *
400      * The [Key] returned by this method should cause [load] to load enough items to fill the
401      * viewport *around* the last accessed position, allowing the next generation to transparently
402      * animate in. The last accessed position can be retrieved via
403      * [state.anchorPosition][PagingState.anchorPosition], which is typically the *top-most* or
404      * *bottom-most* item in the viewport due to access being triggered by binding items as they
405      * scroll into view.
406      *
407      * For example, if items are loaded based on integer position keys, you can return `(
408      * (state.anchorPosition ?: 0) - state.config.initialLoadSize / 2).coerceAtLeast(0)`.
409      *
410      * Alternately, if items contain a key used to load, get the key from the item in the page at
411      * index [state.anchorPosition][PagingState.anchorPosition] then try to center it based on
412      * `state.config.initialLoadSize`.
413      *
414      * @param state [PagingState] of the currently fetched data, which includes the most recently
415      *   accessed position in the list via [PagingState.anchorPosition].
416      * @return [Key] passed to [load] after invalidation used for initial load of the next
417      *   generation. The [Key] returned by [getRefreshKey] should load pages centered around user's
418      *   current viewport. If the correct [Key] cannot be determined, `null` can be returned to
419      *   allow [load] decide what default key to use.
420      */
421     public abstract fun getRefreshKey(state: PagingState<Key, Value>): Key?
422 }
423