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.VisibleForTesting 20 import androidx.annotation.WorkerThread 21 import androidx.arch.core.util.Function 22 import androidx.paging.DataSource.KeyType.POSITIONAL 23 import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED 24 import kotlin.coroutines.resume 25 import kotlinx.coroutines.suspendCancellableCoroutine 26 27 /** 28 * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at 29 * arbitrary page positions. 30 * 31 * Extend PositionalDataSource if you can load pages of a requested size at arbitrary positions, and 32 * provide a fixed item count. If your data source can't support loading arbitrary requested page 33 * sizes (e.g. when network page size constraints are only known at runtime), either use 34 * [PageKeyedDataSource] or [ItemKeyedDataSource], or pass the initial result with the two parameter 35 * [LoadInitialCallback.onResult]. 36 * 37 * Room can generate a Factory of PositionalDataSources for you: 38 * ``` 39 * @Dao 40 * interface UserDao { 41 * @Query("SELECT * FROM user ORDER BY age DESC") 42 * public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc(); 43 * } 44 * ``` 45 * 46 * @param T Type of items being loaded by the [PositionalDataSource]. 47 */ 48 @Deprecated( 49 message = "PositionalDataSource is deprecated and has been replaced by PagingSource", 50 replaceWith = ReplaceWith("PagingSource<Int, T>", "androidx.paging.PagingSource") 51 ) 52 public abstract class PositionalDataSource<T : Any> : DataSource<Int, T>(POSITIONAL) { 53 54 /** Holder object for inputs to [loadInitial]. */ 55 public open class LoadInitialParams( 56 /** 57 * Initial load position requested. 58 * 59 * Note that this may not be within the bounds of your data set, it may need to be adjusted 60 * before you execute your load. 61 */ 62 @JvmField public val requestedStartPosition: Int, 63 /** 64 * Requested number of items to load. 65 * 66 * Note that this may be larger than available data. 67 */ 68 @JvmField public val requestedLoadSize: Int, 69 /** 70 * Defines page size acceptable for return values. 71 * 72 * List of items passed to the callback must be an integer multiple of page size. 73 */ 74 @JvmField public val pageSize: Int, 75 /** 76 * Defines whether placeholders are enabled, and whether the loaded total count will be 77 * ignored. 78 */ 79 @JvmField public val placeholdersEnabled: Boolean 80 ) { 81 init { 82 check(requestedStartPosition >= 0) { "invalid start position: $requestedStartPosition" } 83 check(requestedLoadSize >= 0) { "invalid load size: $requestedLoadSize" } 84 check(pageSize >= 0) { "invalid page size: $pageSize" } 85 } 86 } 87 88 /** Holder object for inputs to [loadRange]. */ 89 public open class LoadRangeParams( 90 /** 91 * START position of data to load. 92 * 93 * Returned data must start at this position. 94 */ 95 @JvmField public val startPosition: Int, 96 /** 97 * Number of items to load. 98 * 99 * Returned data must be of this size, unless at end of the list. 100 */ 101 @JvmField public val loadSize: Int 102 ) 103 104 /** 105 * Callback for [loadInitial] to return data, position, and count. 106 * 107 * A callback should be called only once, and may throw if called again. 108 * 109 * It is always valid for a DataSource loading method that takes a callback to stash the 110 * callback and call it later. This enables DataSources to be fully asynchronous, and to handle 111 * temporary, recoverable error states (such as a network error that can be retried). 112 * 113 * @param T Type of items being loaded. 114 */ 115 public abstract class LoadInitialCallback<T> { 116 /** 117 * Called to pass initial load state from a DataSource. 118 * 119 * Call this method from [loadInitial] function to return data, and inform how many 120 * placeholders should be shown before and after. If counting is cheap compute (for example, 121 * if a network load returns the information regardless), it's recommended to pass the total 122 * size to the totalCount parameter. If placeholders are not requested (when 123 * [LoadInitialParams.placeholdersEnabled] is false), you can instead call [onResult]. 124 * 125 * @param data List of items loaded from the [DataSource]. If this is empty, the 126 * [DataSource] is treated as empty, and no further loads will occur. 127 * @param position Position of the item at the front of the list. If there are N items 128 * before the items in data that can be loaded from this DataSource, pass N. 129 * @param totalCount Total number of items that may be returned from this DataSource. 130 * Includes the number in the initial [data] parameter as well as any items that can be 131 * loaded in front or behind of [data]. 132 */ 133 public abstract fun onResult(data: List<T>, position: Int, totalCount: Int) 134 135 /** 136 * Called to pass initial load state from a DataSource without total count, when 137 * placeholders aren't requested. 138 * 139 * **Note:** This method can only be called when placeholders are disabled (i.e., 140 * [LoadInitialParams.placeholdersEnabled] is `false`). 141 * 142 * Call this method from [loadInitial] function to return data, if position is known but 143 * total size is not. If placeholders are requested, call the three parameter variant: 144 * [onResult]. 145 * 146 * @param data List of items loaded from the [DataSource]. If this is empty, the 147 * [DataSource] is treated as empty, and no further loads will occur. 148 * @param position Position of the item at the front of the list. If there are N items 149 * before the items in data that can be provided by this [DataSource], pass N. 150 */ 151 public abstract fun onResult(data: List<T>, position: Int) 152 } 153 154 /** 155 * Callback for PositionalDataSource [loadRange] to return data. 156 * 157 * A callback should be called only once, and may throw if called again. 158 * 159 * It is always valid for a [DataSource] loading method that takes a callback to stash the 160 * callback and call it later. This enables DataSources to be fully asynchronous, and to handle 161 * temporary, recoverable error states (such as a network error that can be retried). 162 * 163 * @param T Type of items being loaded. 164 */ 165 public abstract class LoadRangeCallback<T> { 166 /** 167 * Called to pass loaded data from [loadRange]. 168 * 169 * @param data List of items loaded from the [DataSource]. Must be same size as requested, 170 * unless at end of list. 171 */ 172 public abstract fun onResult(data: List<T>) 173 } 174 175 public companion object { 176 /** 177 * Helper for computing an initial position in [loadInitial] when total data set size can be 178 * computed ahead of loading. 179 * 180 * The value computed by this function will do bounds checking, page alignment, and 181 * positioning based on initial load size requested. 182 * 183 * Example usage in a [PositionalDataSource] subclass: 184 * ``` 185 * class ItemDataSource extends PositionalDataSource<Item> { 186 * private int computeCount() { 187 * // actual count code here 188 * } 189 * 190 * private List<Item> loadRangeInternal(int startPosition, int loadCount) { 191 * // actual load code here 192 * } 193 * 194 * @Override 195 * public void loadInitial(@NonNull LoadInitialParams params, 196 * @NonNull LoadInitialCallback<Item> callback) { 197 * int totalCount = computeCount(); 198 * int position = computeInitialLoadPosition(params, totalCount); 199 * int loadSize = computeInitialLoadSize(params, position, totalCount); 200 * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount); 201 * } 202 * 203 * @Override 204 * public void loadRange(@NonNull LoadRangeParams params, 205 * @NonNull LoadRangeCallback<Item> callback) { 206 * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize)); 207 * } 208 * } 209 * ``` 210 * 211 * @param params Params passed to [loadInitial], including page size, and requested start / 212 * loadSize. 213 * @param totalCount Total size of the data set. 214 * @return Position to start loading at. 215 * @see [computeInitialLoadSize] 216 */ 217 @JvmStatic 218 public fun computeInitialLoadPosition(params: LoadInitialParams, totalCount: Int): Int { 219 val position = params.requestedStartPosition 220 val initialLoadSize = params.requestedLoadSize 221 val pageSize = params.pageSize 222 223 var pageStart = position / pageSize * pageSize 224 225 // maximum start pos is that which will encompass end of list 226 val maximumLoadPage = 227 (totalCount - initialLoadSize + pageSize - 1) / pageSize * pageSize 228 pageStart = minOf(maximumLoadPage, pageStart) 229 230 // minimum start position is 0 231 pageStart = maxOf(0, pageStart) 232 233 return pageStart 234 } 235 236 /** 237 * Helper for computing an initial load size in [loadInitial] when total data set size can 238 * be computed ahead of loading. 239 * 240 * This function takes the requested load size, and bounds checks it against the value 241 * returned by [computeInitialLoadPosition]. 242 * 243 * Example usage in a [PositionalDataSource] subclass: 244 * ``` 245 * class ItemDataSource extends PositionalDataSource<Item> { 246 * private int computeCount() { 247 * // actual count code here 248 * } 249 * 250 * private List<Item> loadRangeInternal(int startPosition, int loadCount) { 251 * // actual load code here 252 * } 253 * 254 * @Override 255 * public void loadInitial(@NonNull LoadInitialParams params, 256 * @NonNull LoadInitialCallback<Item> callback) { 257 * int totalCount = computeCount(); 258 * int position = computeInitialLoadPosition(params, totalCount); 259 * int loadSize = computeInitialLoadSize(params, position, totalCount); 260 * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount); 261 * } 262 * 263 * @Override 264 * public void loadRange(@NonNull LoadRangeParams params, 265 * @NonNull LoadRangeCallback<Item> callback) { 266 * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize)); 267 * } 268 * } 269 * ``` 270 * 271 * @param params Params passed to [loadInitial], including page size, and requested start / 272 * loadSize. 273 * @param initialLoadPosition Value returned by [computeInitialLoadPosition] 274 * @param totalCount Total size of the data set. 275 * @return Number of items to load. 276 * @see [computeInitialLoadPosition] 277 */ 278 @JvmStatic 279 public fun computeInitialLoadSize( 280 params: LoadInitialParams, 281 initialLoadPosition: Int, 282 totalCount: Int 283 ): Int = minOf(totalCount - initialLoadPosition, params.requestedLoadSize) 284 } 285 286 final override suspend fun load(params: Params<Int>): BaseResult<T> { 287 if (params.type == LoadType.REFRESH) { 288 var initialPosition = 0 289 var initialLoadSize = params.initialLoadSize 290 if (params.key != null) { 291 initialPosition = params.key 292 293 if (params.placeholdersEnabled) { 294 // snap load size to page multiple (minimum two) 295 initialLoadSize = maxOf(initialLoadSize / params.pageSize, 2) * params.pageSize 296 297 // move start so the load is centered around the key, not starting at it 298 val idealStart = initialPosition - initialLoadSize / 2 299 initialPosition = maxOf(0, idealStart / params.pageSize * params.pageSize) 300 } else { 301 // not tiled, so don't try to snap or force multiple of a page size 302 initialPosition = maxOf(0, initialPosition - initialLoadSize / 2) 303 } 304 } 305 val initParams = 306 LoadInitialParams( 307 initialPosition, 308 initialLoadSize, 309 params.pageSize, 310 params.placeholdersEnabled 311 ) 312 return loadInitial(initParams) 313 } else { 314 var startIndex = params.key!! 315 var loadSize = params.pageSize 316 if (params.type == LoadType.PREPEND) { 317 // clamp load size to positive indices only, and shift start index by load size 318 loadSize = minOf(loadSize, startIndex) 319 startIndex -= loadSize 320 } 321 return loadRange(LoadRangeParams(startIndex, loadSize)) 322 } 323 } 324 325 /** 326 * Load initial list data. 327 * 328 * This method is called to load the initial page(s) from the DataSource. 329 * 330 * LoadResult list must be a multiple of pageSize to enable efficient tiling. 331 */ 332 @VisibleForTesting 333 internal suspend fun loadInitial(params: LoadInitialParams) = 334 suspendCancellableCoroutine<BaseResult<T>> { cont -> 335 loadInitial( 336 params, 337 object : LoadInitialCallback<T>() { 338 override fun onResult(data: List<T>, position: Int, totalCount: Int) { 339 if (isInvalid) { 340 // NOTE: this isInvalid check works around 341 // https://issuetracker.google.com/issues/124511903 342 cont.resume(BaseResult.empty()) 343 } else { 344 val nextKey = position + data.size 345 resume( 346 params, 347 BaseResult( 348 data = data, 349 // skip passing prevKey if nothing else to load 350 prevKey = if (position == 0) null else position, 351 // skip passing nextKey if nothing else to load 352 nextKey = if (nextKey == totalCount) null else nextKey, 353 itemsBefore = position, 354 itemsAfter = totalCount - data.size - position 355 ) 356 ) 357 } 358 } 359 360 override fun onResult(data: List<T>, position: Int) { 361 if (isInvalid) { 362 // NOTE: this isInvalid check works around 363 // https://issuetracker.google.com/issues/124511903 364 cont.resume(BaseResult.empty()) 365 } else { 366 resume( 367 params, 368 BaseResult( 369 data = data, 370 // skip passing prevKey if nothing else to load 371 prevKey = if (position == 0) null else position, 372 // can't do same for nextKey, since we don't know if load is 373 // terminal 374 nextKey = position + data.size, 375 itemsBefore = position, 376 itemsAfter = COUNT_UNDEFINED 377 ) 378 ) 379 } 380 } 381 382 private fun resume(params: LoadInitialParams, result: BaseResult<T>) { 383 if (params.placeholdersEnabled) { 384 result.validateForInitialTiling(params.pageSize) 385 } 386 cont.resume(result) 387 } 388 } 389 ) 390 } 391 392 /** 393 * Called to load a range of data from the DataSource. 394 * 395 * This method is called to load additional pages from the DataSource after the 396 * [ItemKeyedDataSource.LoadInitialCallback] passed to dispatchLoadInitial has initialized a 397 * [PagedList]. 398 * 399 * Unlike [ItemKeyedDataSource.loadInitial], this method must return the number of items 400 * requested, at the position requested. 401 */ 402 private suspend fun loadRange(params: LoadRangeParams) = 403 suspendCancellableCoroutine<BaseResult<T>> { cont -> 404 loadRange( 405 params, 406 object : LoadRangeCallback<T>() { 407 override fun onResult(data: List<T>) { 408 // skip passing prevKey if nothing else to load. We only do this for prepend 409 // direction, since 0 as first index is well defined, but max index may not 410 // be 411 val prevKey = if (params.startPosition == 0) null else params.startPosition 412 when { 413 isInvalid -> cont.resume(BaseResult.empty()) 414 else -> 415 cont.resume( 416 BaseResult( 417 data = data, 418 prevKey = prevKey, 419 nextKey = params.startPosition + data.size 420 ) 421 ) 422 } 423 } 424 } 425 ) 426 } 427 428 /** 429 * Load initial list data. 430 * 431 * This method is called to load the initial page(s) from the [DataSource]. 432 * 433 * LoadResult list must be a multiple of pageSize to enable efficient tiling. 434 * 435 * @param params Parameters for initial load, including requested start position, load size, and 436 * page size. 437 * @param callback Callback that receives initial load data, including position and total data 438 * set size. 439 */ 440 @WorkerThread 441 public abstract fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) 442 443 /** 444 * Called to load a range of data from the DataSource. 445 * 446 * This method is called to load additional pages from the DataSource after the 447 * [LoadInitialCallback] passed to dispatchLoadInitial has initialized a [PagedList]. 448 * 449 * Unlike [loadInitial], this method must return the number of items requested, at the position 450 * requested. 451 * 452 * @param params Parameters for load, including start position and load size. 453 * @param callback Callback that receives loaded data. 454 */ 455 @WorkerThread 456 public abstract fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) 457 458 @Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly. 459 internal override val isContiguous = false 460 461 @Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly. 462 internal final override fun getKeyInternal(item: T): Int = 463 throw IllegalStateException("Cannot get key by item in positionalDataSource") 464 465 @Suppress("DEPRECATION") 466 final override fun <V : Any> mapByPage( 467 function: Function<List<T>, List<V>> 468 ): PositionalDataSource<V> = WrapperPositionalDataSource(this, function) 469 470 @Suppress("DEPRECATION") 471 final override fun <V : Any> mapByPage( 472 function: (List<T>) -> List<V> 473 ): PositionalDataSource<V> = mapByPage(Function { function(it) }) 474 475 @Suppress("DEPRECATION") 476 final override fun <V : Any> map(function: Function<T, V>): PositionalDataSource<V> = 477 mapByPage(Function { list -> list.map { function.apply(it) } }) 478 479 @Suppress("DEPRECATION") 480 final override fun <V : Any> map(function: (T) -> V): PositionalDataSource<V> = 481 mapByPage(Function { list -> list.map(function) }) 482 } 483