blob: b898ab61d1227be6ef0b1029d267cbd857939b3b [file] [log] [blame]
/*
* 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) })
}