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