| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.paging |
| |
| import androidx.annotation.RestrictTo |
| import androidx.annotation.VisibleForTesting |
| import androidx.annotation.WorkerThread |
| import androidx.arch.core.util.Function |
| import androidx.paging.DataSource.KeyType.POSITIONAL |
| import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED |
| import kotlinx.coroutines.suspendCancellableCoroutine |
| import kotlin.coroutines.resume |
| |
| /** |
| * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at |
| * arbitrary page positions. |
| * |
| * Extend PositionalDataSource if you can load pages of a requested size at arbitrary positions, |
| * and provide a fixed item count. If your data source can't support loading arbitrary requested |
| * page sizes (e.g. when network page size constraints are only known at runtime), either use |
| * [PageKeyedDataSource] or [ItemKeyedDataSource], or pass the initial result with the two parameter |
| * [LoadInitialCallback.onResult]. |
| * |
| * Room can generate a Factory of PositionalDataSources for you: |
| * ``` |
| * @Dao |
| * interface UserDao { |
| * @Query("SELECT * FROM user ORDER BY age DESC") |
| * public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc(); |
| * } |
| * ``` |
| * |
| * @param T Type of items being loaded by the [PositionalDataSource]. |
| */ |
| @Deprecated( |
| message = "PositionalDataSource is deprecated and has been replaced by PagingSource", |
| replaceWith = ReplaceWith( |
| "PagingSource<Int, T>", |
| "androidx.paging.PagingSource" |
| ) |
| ) |
| abstract class PositionalDataSource<T : Any> : DataSource<Int, T>(POSITIONAL) { |
| |
| /** |
| * Holder object for inputs to [loadInitial]. |
| */ |
| open class LoadInitialParams( |
| /** |
| * Initial load position requested. |
| * |
| * Note that this may not be within the bounds of your data set, it may need to be adjusted |
| * before you execute your load. |
| */ |
| @JvmField |
| val requestedStartPosition: Int, |
| /** |
| * Requested number of items to load. |
| * |
| * Note that this may be larger than available data. |
| */ |
| @JvmField |
| val requestedLoadSize: Int, |
| /** |
| * Defines page size acceptable for return values. |
| * |
| * List of items passed to the callback must be an integer multiple of page size. |
| */ |
| @JvmField |
| val pageSize: Int, |
| /** |
| * Defines whether placeholders are enabled, and whether the loaded total count will be |
| * ignored. |
| */ |
| @JvmField |
| val placeholdersEnabled: Boolean |
| ) |
| |
| /** |
| * Holder object for inputs to [loadRange]. |
| */ |
| open class LoadRangeParams( |
| /** |
| * START position of data to load. |
| * |
| * Returned data must start at this position. |
| */ |
| @JvmField |
| val startPosition: Int, |
| /** |
| * Number of items to load. |
| * |
| * Returned data must be of this size, unless at end of the list. |
| */ |
| @JvmField |
| val loadSize: Int |
| ) |
| |
| /** |
| * Callback for [loadInitial] to return data, position, and count. |
| * |
| * A callback should be called only once, and may throw if called again. |
| * |
| * It is always valid for a DataSource loading method that takes a callback to stash the |
| * callback and call it later. This enables DataSources to be fully asynchronous, and to handle |
| * temporary, recoverable error states (such as a network error that can be retried). |
| * |
| * @param T Type of items being loaded. |
| */ |
| abstract class LoadInitialCallback<T> { |
| /** |
| * Called to pass initial load state from a DataSource. |
| * |
| * Call this method from [loadInitial] function to return data, and inform how many |
| * placeholders should be shown before and after. If counting is cheap compute (for example, |
| * if a network load returns the information regardless), it's recommended to pass the total |
| * size to the totalCount parameter. If placeholders are not requested (when |
| * [LoadInitialParams.placeholdersEnabled] is false), you can instead call [onResult]. |
| * |
| * @param data List of items loaded from the [DataSource]. If this is empty, the |
| * [DataSource] is treated as empty, and no further loads will occur. |
| * @param position Position of the item at the front of the list. If there are N items |
| * before the items in data that can be loaded from this DataSource, pass N. |
| * @param totalCount Total number of items that may be returned from this DataSource. |
| * Includes the number in the initial [data] parameter as well as any items that can be |
| * loaded in front or behind of [data]. |
| */ |
| abstract fun onResult(data: List<T>, position: Int, totalCount: Int) |
| |
| /** |
| * Called to pass initial load state from a DataSource without total count, when |
| * placeholders aren't requested. |
| * |
| * **Note:** This method can only be called when placeholders are disabled (i.e., |
| * [LoadInitialParams.placeholdersEnabled] is `false`). |
| * |
| * Call this method from [loadInitial] function to return data, if position is known but |
| * total size is not. If placeholders are requested, call the three parameter variant: |
| * [onResult]. |
| * |
| * @param data List of items loaded from the [DataSource]. If this is empty, the |
| * [DataSource] is treated as empty, and no further loads will occur. |
| * @param position Position of the item at the front of the list. If there are N items |
| * before the items in data that can be provided by this [DataSource], pass N. |
| */ |
| abstract fun onResult(data: List<T>, position: Int) |
| } |
| |
| /** |
| * Callback for PositionalDataSource [loadRange] to return data. |
| * |
| * A callback should be called only once, and may throw if called again. |
| * |
| * It is always valid for a [DataSource] loading method that takes a callback to stash the |
| * callback and call it later. This enables DataSources to be fully asynchronous, and to handle |
| * temporary, recoverable error states (such as a network error that can be retried). |
| * |
| * @param T Type of items being loaded. |
| */ |
| abstract class LoadRangeCallback<T> { |
| /** |
| * Called to pass loaded data from [loadRange]. |
| * |
| * @param data List of items loaded from the [DataSource]. Must be same size as requested, |
| * unless at end of list. |
| */ |
| abstract fun onResult(data: List<T>) |
| } |
| |
| /** |
| * @suppress |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| companion object { |
| /** |
| * Helper for computing an initial position in [loadInitial] when total data set size can be |
| * computed ahead of loading. |
| * |
| * The value computed by this function will do bounds checking, page alignment, and |
| * positioning based on initial load size requested. |
| * |
| * Example usage in a [PositionalDataSource] subclass: |
| * ``` |
| * class ItemDataSource extends PositionalDataSource<Item> { |
| * private int computeCount() { |
| * // actual count code here |
| * } |
| * |
| * private List<Item> loadRangeInternal(int startPosition, int loadCount) { |
| * // actual load code here |
| * } |
| * |
| * @Override |
| * public void loadInitial(@NonNull LoadInitialParams params, |
| * @NonNull LoadInitialCallback<Item> callback) { |
| * int totalCount = computeCount(); |
| * int position = computeInitialLoadPosition(params, totalCount); |
| * int loadSize = computeInitialLoadSize(params, position, totalCount); |
| * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount); |
| * } |
| * |
| * @Override |
| * public void loadRange(@NonNull LoadRangeParams params, |
| * @NonNull LoadRangeCallback<Item> callback) { |
| * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize)); |
| * } |
| * } |
| * ``` |
| * |
| * @param params Params passed to [loadInitial], including page size, and requested start / |
| * loadSize. |
| * @param totalCount Total size of the data set. |
| * @return Position to start loading at. |
| * |
| * @see [computeInitialLoadSize] |
| */ |
| @JvmStatic |
| fun computeInitialLoadPosition(params: LoadInitialParams, totalCount: Int): Int { |
| val position = params.requestedStartPosition |
| val initialLoadSize = params.requestedLoadSize |
| val pageSize = params.pageSize |
| |
| var pageStart = position / pageSize * pageSize |
| |
| // maximum start pos is that which will encompass end of list |
| val maximumLoadPage = |
| (totalCount - initialLoadSize + pageSize - 1) / pageSize * pageSize |
| pageStart = minOf(maximumLoadPage, pageStart) |
| |
| // minimum start position is 0 |
| pageStart = maxOf(0, pageStart) |
| |
| return pageStart |
| } |
| |
| /** |
| * Helper for computing an initial load size in [loadInitial] when total data set size can |
| * be computed ahead of loading. |
| * |
| * This function takes the requested load size, and bounds checks it against the value |
| * returned by [computeInitialLoadPosition]. |
| * |
| * Example usage in a [PositionalDataSource] subclass: |
| * ``` |
| * class ItemDataSource extends PositionalDataSource<Item> { |
| * private int computeCount() { |
| * // actual count code here |
| * } |
| * |
| * private List<Item> loadRangeInternal(int startPosition, int loadCount) { |
| * // actual load code here |
| * } |
| * |
| * @Override |
| * public void loadInitial(@NonNull LoadInitialParams params, |
| * @NonNull LoadInitialCallback<Item> callback) { |
| * int totalCount = computeCount(); |
| * int position = computeInitialLoadPosition(params, totalCount); |
| * int loadSize = computeInitialLoadSize(params, position, totalCount); |
| * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount); |
| * } |
| * |
| * @Override |
| * public void loadRange(@NonNull LoadRangeParams params, |
| * @NonNull LoadRangeCallback<Item> callback) { |
| * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize)); |
| * } |
| * } |
| * ``` |
| * |
| * @param params Params passed to [loadInitial], including page size, and requested start / |
| * loadSize. |
| * @param initialLoadPosition Value returned by [computeInitialLoadPosition] |
| * @param totalCount Total size of the data set. |
| * @return Number of items to load. |
| * |
| * @see [computeInitialLoadPosition] |
| */ |
| @JvmStatic |
| fun computeInitialLoadSize( |
| params: LoadInitialParams, |
| initialLoadPosition: Int, |
| totalCount: Int |
| ) = minOf(totalCount - initialLoadPosition, params.requestedLoadSize) |
| } |
| |
| @Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly. |
| internal final override suspend fun load(params: Params<Int>): BaseResult<T> { |
| if (params.type == LoadType.REFRESH) { |
| var initialPosition = 0 |
| var initialLoadSize = params.initialLoadSize |
| if (params.key != null) { |
| initialPosition = params.key |
| |
| if (params.placeholdersEnabled) { |
| // snap load size to page multiple (minimum two) |
| initialLoadSize = |
| maxOf(initialLoadSize / params.pageSize, 2) * params.pageSize |
| |
| // move start so the load is centered around the key, not starting at it |
| val idealStart = initialPosition - initialLoadSize / 2 |
| initialPosition = maxOf(0, idealStart / params.pageSize * params.pageSize) |
| } else { |
| // not tiled, so don't try to snap or force multiple of a page size |
| initialPosition -= initialLoadSize / 2 |
| } |
| } |
| val initParams = LoadInitialParams( |
| initialPosition, |
| initialLoadSize, |
| params.pageSize, |
| params.placeholdersEnabled |
| ) |
| return loadInitial(initParams) |
| } else { |
| var startIndex = params.key!! |
| var loadSize = params.pageSize |
| if (params.type == LoadType.START) { |
| // clamp load size to positive indices only, and shift start index by load size |
| loadSize = minOf(loadSize, startIndex) |
| startIndex -= loadSize |
| } |
| return loadRange(LoadRangeParams(startIndex, loadSize)) |
| } |
| } |
| |
| /** |
| * Load initial list data. |
| * |
| * This method is called to load the initial page(s) from the DataSource. |
| * |
| * LoadResult list must be a multiple of pageSize to enable efficient tiling. |
| */ |
| @VisibleForTesting |
| internal suspend fun loadInitial(params: LoadInitialParams) = |
| suspendCancellableCoroutine<BaseResult<T>> { cont -> |
| loadInitial(params, object : LoadInitialCallback<T>() { |
| override fun onResult(data: List<T>, position: Int, totalCount: Int) { |
| if (isInvalid) { |
| // NOTE: this isInvalid check works around |
| // https://issuetracker.google.com/issues/124511903 |
| cont.resume(BaseResult.empty()) |
| } else { |
| resume( |
| params, BaseResult( |
| data = data, |
| prevKey = position, |
| nextKey = position + data.size, |
| itemsBefore = position, |
| itemsAfter = totalCount - data.size - position |
| ) |
| ) |
| } |
| } |
| |
| override fun onResult(data: List<T>, position: Int) { |
| if (isInvalid) { |
| // NOTE: this isInvalid check works around |
| // https://issuetracker.google.com/issues/124511903 |
| cont.resume(BaseResult.empty()) |
| } else { |
| // always pass prevKey/nextKey, since we let position |
| resume( |
| params, BaseResult( |
| data = data, |
| prevKey = position, |
| nextKey = position + data.size, |
| itemsBefore = position, |
| itemsAfter = COUNT_UNDEFINED |
| ) |
| ) |
| } |
| } |
| |
| private fun resume(params: LoadInitialParams, result: BaseResult<T>) { |
| if (params.placeholdersEnabled) { |
| result.validateForInitialTiling(params.pageSize) |
| } |
| cont.resume(result) |
| } |
| }) |
| } |
| |
| /** |
| * Called to load a range of data from the DataSource. |
| * |
| * This method is called to load additional pages from the DataSource after the |
| * [ItemKeyedDataSource.LoadInitialCallback] passed to dispatchLoadInitial has initialized a |
| * [PagedList]. |
| * |
| * Unlike [ItemKeyedDataSource.loadInitial], this method must return the number of items |
| * requested, at the position requested. |
| */ |
| private suspend fun loadRange(params: LoadRangeParams) = |
| suspendCancellableCoroutine<BaseResult<T>> { cont -> |
| loadRange(params, object : LoadRangeCallback<T>() { |
| override fun onResult(data: List<T>) { |
| when { |
| isInvalid -> cont.resume(BaseResult.empty()) |
| else -> cont.resume( |
| BaseResult( |
| data = data, |
| prevKey = params.startPosition, |
| nextKey = params.startPosition + data.size |
| ) |
| ) |
| } |
| } |
| }) |
| } |
| |
| /** |
| * Load initial list data. |
| * |
| * This method is called to load the initial page(s) from the [DataSource]. |
| * |
| * LoadResult list must be a multiple of pageSize to enable efficient tiling. |
| * |
| * @param params Parameters for initial load, including requested start position, load size, and |
| * page size. |
| * @param callback Callback that receives initial load data, including position and total data |
| * set size. |
| */ |
| @WorkerThread |
| abstract fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) |
| |
| /** |
| * Called to load a range of data from the DataSource. |
| * |
| * This method is called to load additional pages from the DataSource after the |
| * [LoadInitialCallback] passed to dispatchLoadInitial has initialized a [PagedList]. |
| * |
| * Unlike [loadInitial], this method must return the number of items requested, at the position |
| * requested. |
| * |
| * @param params Parameters for load, including start position and load size. |
| * @param callback Callback that receives loaded data. |
| */ |
| @WorkerThread |
| abstract fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) |
| |
| @Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly. |
| internal override val isContiguous = false |
| |
| @Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly. |
| internal final override fun getKeyInternal(item: T): Int = |
| throw IllegalStateException("Cannot get key by item in positionalDataSource") |
| |
| @Suppress("DEPRECATION") |
| final override fun <V : Any> mapByPage( |
| function: Function<List<T>, List<V>> |
| ): PositionalDataSource<V> = WrapperPositionalDataSource(this, function) |
| |
| @Suppress("DEPRECATION") |
| final override fun <V : Any> mapByPage( |
| function: (List<T>) -> List<V> |
| ): PositionalDataSource<V> = mapByPage(Function { function(it) }) |
| |
| @Suppress("DEPRECATION") |
| final override fun <V : Any> map(function: Function<T, V>): PositionalDataSource<V> = |
| mapByPage(Function { list -> list.map { function.apply(it) } }) |
| |
| @Suppress("DEPRECATION") |
| final override fun <V : Any> map(function: (T) -> V): PositionalDataSource<V> = |
| mapByPage(Function { list -> list.map(function) }) |
| } |