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.AnyThread 20 import androidx.annotation.VisibleForTesting 21 import androidx.annotation.WorkerThread 22 import androidx.arch.core.util.Function 23 import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED 24 import kotlinx.coroutines.CoroutineDispatcher 25 import kotlinx.coroutines.Dispatchers 26 27 /** 28 * Base class for loading pages of snapshot data into a [PagedList]. 29 * 30 * DataSource is queried to load pages of content into a [PagedList]. A PagedList can grow as it 31 * loads more data, but the data loaded cannot be updated. If the underlying data set is modified, a 32 * new PagedList / DataSource pair must be created to represent the new data. 33 * 34 * ### Loading Pages 35 * 36 * PagedList queries data from its DataSource in response to loading hints. PagedListAdapter calls 37 * [PagedList.loadAround] to load content as the user scrolls in a RecyclerView. 38 * 39 * To control how and when a PagedList queries data from its DataSource, see [PagedList.Config]. The 40 * Config object defines things like load sizes and prefetch distance. 41 * 42 * ### Updating Paged Data 43 * 44 * A PagedList / DataSource pair are a snapshot of the data set. A new pair of PagedList / 45 * DataSource must be created if an update occurs, such as a reorder, insert, delete, or content 46 * update occurs. A DataSource 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 PagedList / DataSource pair would be created to load data from the new state of the Database 49 * query. 50 * 51 * To page in data that doesn't update, you can create a single DataSource, and pass it to a single 52 * PagedList. For example, loading from network when the network's paging API doesn't provide 53 * updates. 54 * 55 * To page in data from a source that does provide updates, you can create a [DataSource.Factory], 56 * where each DataSource created is invalidated when an update to the data set occurs that makes the 57 * current snapshot invalid. For example, when paging a query from the Database, and the table being 58 * queried inserts or removes items. You can also use a DataSource.Factory to provide multiple 59 * versions of network-paged lists. If reloading all content (e.g. in response to an action like 60 * swipe-to-refresh) is required to get a new version of data, you can connect an explicit refresh 61 * signal to call [invalidate] on the current [DataSource]. 62 * 63 * If you have more granular update signals, such as a network API signaling an update to a single 64 * item in the list, it's recommended to load data from network into memory. Then present that data 65 * to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory copy 66 * changes, invalidate the previous DataSource, and a new one wrapping the new state of the snapshot 67 * can be created. 68 * 69 * ### Implementing a DataSource 70 * 71 * To implement, extend one of the subclasses: [PageKeyedDataSource], [ItemKeyedDataSource], or 72 * [PositionalDataSource]. 73 * 74 * Use [PageKeyedDataSource] if pages you load embed keys for loading adjacent pages. For example a 75 * network response that returns some items, and a next/previous page links. 76 * 77 * Use [ItemKeyedDataSource] if you need to use data from item `N-1` to load item `N`. For example, 78 * if requesting the backend for the next comments in the list requires the ID or timestamp of the 79 * most recent loaded comment, or if querying the next users from a name-sorted database query 80 * requires the name and unique ID of the previous. 81 * 82 * Use [PositionalDataSource] if you can load pages of a requested size at arbitrary positions, and 83 * provide a fixed item count. PositionalDataSource supports querying pages at arbitrary positions, 84 * so can provide data to PagedLists in arbitrary order. Note that PositionalDataSource is required 85 * to respect page size for efficient tiling. If you want to override page size (e.g. when network 86 * page size constraints are only known at runtime), use one of the other DataSource classes. 87 * 88 * Because a `null` item indicates a placeholder in [PagedList], DataSource may not return `null` 89 * items in lists that it loads. This is so that users of the PagedList can differentiate unloaded 90 * placeholder items from content that has been paged in. 91 * 92 * @param Key Unique identifier for item loaded from DataSource. Often an integer to represent 93 * position in data set. Note - this is distinct from e.g. Room's `<Value>` Value type loaded by 94 * the DataSource. 95 */ 96 public abstract class DataSource<Key : Any, Value : Any> 97 // Since we currently rely on implementation details of two implementations, prevent external 98 // subclassing, except through exposed subclasses. 99 internal constructor(internal val type: KeyType) { 100 101 private val invalidateCallbackTracker = 102 InvalidateCallbackTracker<InvalidatedCallback>( 103 callbackInvoker = { it.onInvalidated() }, 104 invalidGetter = { isInvalid }, 105 ) 106 107 internal val invalidateCallbackCount: Int 108 @VisibleForTesting get() = invalidateCallbackTracker.callbackCount() 109 110 /** @return `true` if the data source is invalid, and can no longer be queried for data. */ 111 public open val isInvalid: Boolean 112 @WorkerThread get() = invalidateCallbackTracker.invalid 113 114 /** 115 * Factory for DataSources. 116 * 117 * Data-loading systems of an application or library can implement this interface to allow 118 * `LiveData<PagedList>`s to be created. For example, Room can provide a [DataSource.Factory] 119 * for a given SQL query: 120 * ``` 121 * @Dao 122 * interface UserDao { 123 * @Query("SELECT * FROM user ORDER BY lastName ASC") 124 * public abstract DataSource.Factory<Integer, User> usersByLastName(); 125 * } 126 * ``` 127 * 128 * In the above sample, `Integer` is used because it is the `Key` type of PositionalDataSource. 129 * Currently, Room uses the `LIMIT`/`OFFSET` SQL keywords to page a large query with a 130 * PositionalDataSource. 131 * 132 * @param Key Key identifying items in DataSource. 133 * @param Value Type of items in the list loaded by the DataSources. 134 */ 135 public abstract class Factory<Key : Any, Value : Any> { 136 /** 137 * Create a [DataSource]. 138 * 139 * The [DataSource] should invalidate itself if the snapshot is no longer valid. If a 140 * [DataSource] becomes invalid, the only way to query more data is to create a new 141 * [DataSource] from the Factory. 142 * 143 * [androidx.paging.LivePagedListBuilder] for example will construct a new PagedList and 144 * DataSource when the current DataSource is invalidated, and pass the new PagedList through 145 * the `LiveData<PagedList>` to observers. 146 * 147 * @return the new DataSource. 148 */ 149 public abstract fun create(): DataSource<Key, Value> 150 151 /** 152 * Applies the given function to each value emitted by DataSources produced by this Factory. 153 * 154 * Same as [mapByPage], but operates on individual items. 155 * 156 * @param function Function that runs on each loaded item, returning items of a potentially 157 * new type. 158 * @param ToValue Type of items produced by the new [DataSource], from the passed function. 159 * @return A new [DataSource.Factory], which transforms items using the given function. 160 * @see mapByPage 161 * @see DataSource.map 162 * @see DataSource.mapByPage 163 */ 164 public open fun <ToValue : Any> map( 165 function: Function<Value, ToValue> 166 ): Factory<Key, ToValue> { 167 return mapByPage(Function { list -> list.map { function.apply(it) } }) 168 } 169 170 /** 171 * Applies the given function to each value emitted by DataSources produced by this Factory. 172 * 173 * An overload of [map] that accepts a kotlin function type. 174 * 175 * Same as [mapByPage], but operates on individual items. 176 * 177 * @param function Function that runs on each loaded item, returning items of a potentially 178 * new type. 179 * @param ToValue Type of items produced by the new [DataSource], from the passed function. 180 * @return A new [DataSource.Factory], which transforms items using the given function. 181 * @see mapByPage 182 * @see DataSource.map 183 * @see DataSource.mapByPage 184 */ 185 @JvmSynthetic // hidden to preserve Java source compat with arch.core.util.Function variant 186 public open fun <ToValue : Any> map(function: (Value) -> ToValue): Factory<Key, ToValue> { 187 return mapByPage(Function { list -> list.map(function) }) 188 } 189 190 /** 191 * Applies the given function to each value emitted by DataSources produced by this Factory. 192 * 193 * Same as [map], but allows for batch conversions. 194 * 195 * @param function Function that runs on each loaded page, returning items of a potentially 196 * new type. 197 * @param ToValue Type of items produced by the new [DataSource], from the passed function. 198 * @return A new [DataSource.Factory], which transforms items using the given function. 199 * @see map 200 * @see DataSource.map 201 * @see DataSource.mapByPage 202 */ 203 public open fun <ToValue : Any> mapByPage( 204 function: Function<List<Value>, List<ToValue>> 205 ): Factory<Key, ToValue> = 206 object : Factory<Key, ToValue>() { 207 override fun create(): DataSource<Key, ToValue> = 208 this@Factory.create().mapByPage(function) 209 } 210 211 /** 212 * Applies the given function to each value emitted by DataSources produced by this Factory. 213 * 214 * An overload of [mapByPage] that accepts a kotlin function type. 215 * 216 * Same as [map], but allows for batch conversions. 217 * 218 * @param function Function that runs on each loaded page, returning items of a potentially 219 * new type. 220 * @param ToValue Type of items produced by the new [DataSource], from the passed function. 221 * @return A new [DataSource.Factory], which transforms items using the given function. 222 * @see map 223 * @see DataSource.map 224 * @see DataSource.mapByPage 225 */ 226 @JvmSynthetic // hidden to preserve Java source compat with arch.core.util.Function variant 227 public open fun <ToValue : Any> mapByPage( 228 function: (List<Value>) -> List<ToValue> 229 ): Factory<Key, ToValue> = mapByPage(Function { function(it) }) 230 231 @JvmOverloads 232 public fun asPagingSourceFactory( 233 fetchDispatcher: CoroutineDispatcher = Dispatchers.IO 234 ): () -> PagingSource<Key, Value> = 235 SuspendingPagingSourceFactory( 236 delegate = { LegacyPagingSource(fetchDispatcher, create()) }, 237 dispatcher = fetchDispatcher 238 ) 239 } 240 241 /** 242 * Applies the given function to each value emitted by the DataSource. 243 * 244 * Same as [map], but allows for batch conversions. 245 * 246 * @param function Function that runs on each loaded page, returning items of a potentially new 247 * type. 248 * @param ToValue Type of items produced by the new DataSource, from the passed function. 249 * @return A new DataSource, which transforms items using the given function. 250 * @see map 251 * @see DataSource.Factory.map 252 * @see DataSource.Factory.mapByPage 253 */ 254 public open fun <ToValue : Any> mapByPage( 255 function: Function<List<Value>, List<ToValue>> 256 ): DataSource<Key, ToValue> = WrapperDataSource(this, function) 257 258 /** 259 * Applies the given function to each value emitted by the DataSource. 260 * 261 * An overload of [mapByPage] that accepts a kotlin function type. 262 * 263 * Same as [map], but allows for batch conversions. 264 * 265 * @param function Function that runs on each loaded page, returning items of a potentially new 266 * type. 267 * @param ToValue Type of items produced by the new DataSource, from the passed function. 268 * @return A new [DataSource], which transforms items using the given function. 269 * @see map 270 * @see DataSource.Factory.map 271 * @see DataSource.Factory.mapByPage 272 */ 273 @JvmSynthetic // hidden to preserve Java source compat with arch.core.util.Function variant 274 public open fun <ToValue : Any> mapByPage( 275 function: (List<Value>) -> List<ToValue> 276 ): DataSource<Key, ToValue> = mapByPage(Function { function(it) }) 277 278 /** 279 * Applies the given function to each value emitted by the DataSource. 280 * 281 * Same as [mapByPage], but operates on individual items. 282 * 283 * @param function Function that runs on each loaded item, returning items of a potentially new 284 * type. 285 * @param ToValue Type of items produced by the new DataSource, from the passed function. 286 * @return A new DataSource, which transforms items using the given function. 287 * @see mapByPage 288 * @see DataSource.Factory.map 289 * @see DataSource.Factory.mapByPage 290 */ 291 public open fun <ToValue : Any> map( 292 function: Function<Value, ToValue> 293 ): DataSource<Key, ToValue> { 294 return mapByPage { list -> list.map { function.apply(it) } } 295 } 296 297 /** 298 * Applies the given function to each value emitted by the DataSource. 299 * 300 * An overload of [map] that accepts a kotlin function type. 301 * 302 * Same as [mapByPage], but operates on individual items. 303 * 304 * @param function Function that runs on each loaded item, returning items of a potentially new 305 * type. 306 * @param ToValue Type of items produced by the new DataSource, from the passed function. 307 * @return A new DataSource, which transforms items using the given function. 308 * @see mapByPage 309 * @see DataSource.Factory.map 310 */ 311 @JvmSynthetic // hidden to preserve Java source compat with arch.core.util.Function variant 312 public open fun <ToValue : Any> map(function: (Value) -> ToValue): DataSource<Key, ToValue> = 313 map(Function { function(it) }) 314 315 /** 316 * Returns true if the data source guaranteed to produce a contiguous set of items, never 317 * producing gaps. 318 */ 319 internal open val isContiguous = true 320 321 internal open val supportsPageDropping = true 322 323 /** 324 * Invalidation callback for [DataSource]. 325 * 326 * Used to signal when a [DataSource] a data source has become invalid, and that a new data 327 * source is needed to continue loading data. 328 */ 329 public fun interface InvalidatedCallback { 330 /** 331 * Called when the data backing the list has become invalid. This callback is typically used 332 * to signal that a new data source is needed. 333 * 334 * This callback will be invoked on the thread that calls [invalidate]. It is valid for the 335 * data source to invalidate itself during its load methods, or for an outside source to 336 * invalidate it. 337 */ 338 @AnyThread public fun onInvalidated() 339 } 340 341 /** 342 * Add a callback to invoke when the DataSource is first invalidated. 343 * 344 * Once invalidated, a data source will not become valid again. 345 * 346 * A data source will only invoke its callbacks once - the first time [invalidate] is called, on 347 * that thread. 348 * 349 * If this [DataSource] is already invalid, the provided [onInvalidatedCallback] will be 350 * triggered immediately. 351 * 352 * @param onInvalidatedCallback The callback, will be invoked on thread that invalidates the 353 * [DataSource]. 354 */ 355 @AnyThread 356 @Suppress("RegistrationName") 357 public open fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) { 358 invalidateCallbackTracker.registerInvalidatedCallback(onInvalidatedCallback) 359 } 360 361 /** 362 * Remove a previously added invalidate callback. 363 * 364 * @param onInvalidatedCallback The previously added callback. 365 */ 366 @AnyThread 367 @Suppress("RegistrationName") 368 public open fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) { 369 invalidateCallbackTracker.unregisterInvalidatedCallback(onInvalidatedCallback) 370 } 371 372 /** 373 * Signal the data source to stop loading, and notify its callback. 374 * 375 * If invalidate has already been called, this method does nothing. 376 */ 377 @AnyThread 378 public open fun invalidate() { 379 invalidateCallbackTracker.invalidate() 380 } 381 382 /** 383 * @param K Type of the key used to query the [DataSource]. 384 * @property key Can be `null` for init, otherwise non-null 385 */ 386 internal class Params<K : Any> 387 internal constructor( 388 internal val type: LoadType, 389 val key: K?, 390 val initialLoadSize: Int, 391 val placeholdersEnabled: Boolean, 392 val pageSize: Int 393 ) { 394 init { 395 if (type != LoadType.REFRESH && key == null) { 396 throw IllegalArgumentException("Key must be non-null for prepend/append") 397 } 398 } 399 } 400 401 /** @param Value Type of the data produced by a [DataSource]. */ 402 internal class BaseResult<Value : Any> 403 internal constructor( 404 @JvmField val data: List<Value>, 405 val prevKey: Any?, 406 val nextKey: Any?, 407 val itemsBefore: Int = COUNT_UNDEFINED, 408 val itemsAfter: Int = COUNT_UNDEFINED 409 ) { 410 init { 411 if (itemsBefore < 0 && itemsBefore != COUNT_UNDEFINED) { 412 throw IllegalArgumentException("Position must be non-negative") 413 } 414 if (data.isEmpty() && (itemsBefore > 0 || itemsAfter > 0)) { 415 // If non-initial, itemsBefore, itemsAfter are COUNT_UNDEFINED 416 throw IllegalArgumentException( 417 "Initial result cannot be empty if items are present in data set." 418 ) 419 } 420 if (itemsAfter < 0 && itemsAfter != COUNT_UNDEFINED) { 421 throw IllegalArgumentException( 422 "List size + position too large, last item in list beyond totalCount." 423 ) 424 } 425 } 426 427 /** 428 * While it may seem unnecessary to do this validation now that tiling is gone, we do this 429 * to ensure consistency with 2.1, and to ensure all loadRanges have the same page size. 430 */ 431 internal fun validateForInitialTiling(pageSize: Int) { 432 if (itemsBefore == COUNT_UNDEFINED || itemsAfter == COUNT_UNDEFINED) { 433 throw IllegalStateException( 434 "Placeholders requested, but totalCount not provided. Please call the" + 435 " three-parameter onResult method, or disable placeholders in the" + 436 " PagedList.Config" 437 ) 438 } 439 440 if (itemsAfter > 0 && data.size % pageSize != 0) { 441 val totalCount = itemsBefore + data.size + itemsAfter 442 throw IllegalArgumentException( 443 "PositionalDataSource requires initial load size to be a multiple of page" + 444 " size to support internal tiling. loadSize ${data.size}, position" + 445 " $itemsBefore, totalCount $totalCount, pageSize $pageSize" 446 ) 447 } 448 if (itemsBefore % pageSize != 0) { 449 throw IllegalArgumentException( 450 "Initial load must be pageSize aligned.Position = $itemsBefore, pageSize =" + 451 " $pageSize" 452 ) 453 } 454 } 455 456 override fun equals(other: Any?) = 457 when (other) { 458 is BaseResult<*> -> 459 data == other.data && 460 prevKey == other.prevKey && 461 nextKey == other.nextKey && 462 itemsBefore == other.itemsBefore && 463 itemsAfter == other.itemsAfter 464 else -> false 465 } 466 467 internal companion object { 468 internal fun <T : Any> empty() = BaseResult(emptyList<T>(), null, null, 0, 0) 469 470 internal fun <ToValue : Any, Value : Any> convert( 471 result: BaseResult<ToValue>, 472 function: Function<List<ToValue>, List<Value>> 473 ) = 474 BaseResult( 475 data = convert(function, result.data), 476 prevKey = result.prevKey, 477 nextKey = result.nextKey, 478 itemsBefore = result.itemsBefore, 479 itemsAfter = result.itemsAfter 480 ) 481 } 482 } 483 484 internal enum class KeyType { 485 POSITIONAL, 486 PAGE_KEYED, 487 ITEM_KEYED 488 } 489 490 internal abstract suspend fun load(params: Params<Key>): BaseResult<Value> 491 492 internal abstract fun getKeyInternal(item: Value): Key 493 494 internal companion object { 495 internal fun <A, B> convert( 496 function: Function<List<A>, List<B>>, 497 source: List<A> 498 ): List<B> { 499 val dest = function.apply(source) 500 if (dest.size != source.size) { 501 throw IllegalStateException( 502 "Invalid Function $function changed return size. This is not supported." 503 ) 504 } 505 return dest 506 } 507 } 508 } 509