blob: 848486fc4675cb582d8485b7a9117f8ea335de47 [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.arch.core.util.Function
import androidx.paging.DataSource.KeyType.PAGE_KEYED
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
/**
* Incremental data loader for page-keyed content, where requests return keys for next/previous
* pages.
*
* Implement a [DataSource] using [PageKeyedDataSource] if you need to use data from page `N - 1` to
* load page `N`. This is common, for example, in network APIs that include a next/previous link or
* key with each page load.
*
* The `InMemoryByPageRepository` in the
* [PagingWithNetworkSample](https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md)
* shows how to implement a network PageKeyedDataSource using
* [Retrofit](https://square.github.io/retrofit/), while
* handling swipe-to-refresh, network errors, and retry.
*
* @param Key Type of data used to query Value types out of the [DataSource].
* @param Value Type of items being loaded by the [DataSource].
*/
@Deprecated(
message = "PageKeyedDataSource is deprecated and has been replaced by PagingSource",
replaceWith = ReplaceWith(
"PagingSource<Key, Value>",
"androidx.paging.PagingSource"
)
)
abstract class PageKeyedDataSource<Key : Any, Value : Any> : DataSource<Key, Value>(PAGE_KEYED) {
/**
* Holder object for inputs to [loadInitial].
*
* @param Key Type of data used to query pages.
* @property requestedLoadSize Requested number of items to load.
*
* Note that this may be larger than available data.
* @property placeholdersEnabled Defines whether placeholders are enabled, and whether the
* loaded total count will be ignored.
*/
open class LoadInitialParams<Key : Any>(
@JvmField val requestedLoadSize: Int,
@JvmField val placeholdersEnabled: Boolean
)
/**
* Holder object for inputs to [loadBefore] and [loadAfter].
*
* @param Key Type of data used to query pages.
* @property key Load items before/after this key.
*
* Returned data must begin directly adjacent to this position.
* @property requestedLoadSize Requested number of items to load.
*
* Returned page can be of this size, but it may be altered if that is easier, e.g. a network
* data source where the backend defines page size.
*/
open class LoadParams<Key : Any>(@JvmField val key: Key, @JvmField val requestedLoadSize: Int)
/**
* Callback for [loadInitial] to return data and, optionally, position/count information.
*
* A callback can be called only once, and will throw if called again.
*
* If you can compute the number of items in the data set before and after the loaded range,
* call the five parameter [onResult] to pass that information. You can skip passing this
* information by calling the three parameter [onResult], either if it's difficult to compute,
* or if [LoadInitialParams.placeholdersEnabled] is `false`, so the positioning information will
* be ignored.
*
* 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 Key Type of data used to query pages.
* @param Value Type of items being loaded.
*/
abstract class LoadInitialCallback<Key, Value> {
/**
* Called to pass initial load state from a DataSource.
*
* Call this method from your DataSource's `loadInitial` function to return data,
* and inform how many placeholders should be shown before and after. If counting is cheap
* to compute (for example, if a network load returns the information regardless), it's
* recommended to pass data back through this method.
*
* It is always valid to pass a different amount of data than what is requested. Pass an
* empty list if there is no more data to load.
*
* @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<Value>,
position: Int,
totalCount: Int,
previousPageKey: Key?,
nextPageKey: Key?
)
/**
* Called to pass loaded data from a DataSource.
*
* Call this from [loadInitial] to initialize without counting available data, or supporting
* placeholders.
*
* It is always valid to pass a different amount of data than what is requested. Pass an
* empty list if there is no more data to load.
*
* @param data List of items loaded from the [PageKeyedDataSource].
* @param previousPageKey Key for page before the initial load result, or `null` if no more
* data can be loaded before.
* @param nextPageKey Key for page after the initial load result, or `null` if no more data
* can be loaded after.
*/
abstract fun onResult(data: List<Value>, previousPageKey: Key?, nextPageKey: Key?)
}
/**
* Callback for [loadBefore] and [loadAfter] to return data.
*
* A callback can be called only once, and will 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 Key Type of data used to query pages.
* @param Value Type of items being loaded.
*/
abstract class LoadCallback<Key, Value> {
/**
* Called to pass loaded data from a [DataSource].
*
* Call this method from your PageKeyedDataSource's [loadBefore] and [loadAfter] methods to
* return data.
*
* It is always valid to pass a different amount of data than what is requested. Pass an
* empty list if there is no more data to load.
*
* Pass the key for the subsequent page to load to adjacentPageKey. For example, if you've
* loaded a page in [loadBefore], pass the key for the previous page, or `null` if the
* loaded page is the first. If in [loadAfter], pass the key for the next page, or `null`
* if the loaded page is the last.
*
* @param data List of items loaded from the PageKeyedDataSource.
* @param adjacentPageKey Key for subsequent page load (previous page in [loadBefore] / next
* page in [loadAfter]), or `null` if there are no more pages to load in the current load
* direction.
*/
abstract fun onResult(data: List<Value>, adjacentPageKey: Key?)
}
/**
* @throws [IllegalArgumentException] when passed an unsupported load type.
*/
@Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly.
internal final override suspend fun load(params: Params<Key>): BaseResult<Value> = when {
params.type == LoadType.REFRESH -> loadInitial(
LoadInitialParams(
params.initialLoadSize,
params.placeholdersEnabled
)
)
params.key == null -> BaseResult.empty()
params.type == LoadType.START -> loadBefore(LoadParams(params.key, params.pageSize))
params.type == LoadType.END -> loadAfter(LoadParams(params.key, params.pageSize))
else -> throw IllegalArgumentException("Unsupported type " + params.type.toString())
}
private suspend fun loadInitial(params: LoadInitialParams<Key>) =
suspendCancellableCoroutine<BaseResult<Value>> { cont ->
loadInitial(params, object : LoadInitialCallback<Key, Value>() {
override fun onResult(
data: List<Value>,
position: Int,
totalCount: Int,
previousPageKey: Key?,
nextPageKey: Key?
) {
cont.resume(
BaseResult(
data = data,
prevKey = previousPageKey,
nextKey = nextPageKey,
itemsBefore = position,
itemsAfter = totalCount - data.size - position
)
)
}
override fun onResult(data: List<Value>, previousPageKey: Key?, nextPageKey: Key?) {
cont.resume(BaseResult(data, previousPageKey, nextPageKey))
}
})
}
private suspend fun loadBefore(params: LoadParams<Key>) =
suspendCancellableCoroutine<BaseResult<Value>> { cont ->
loadBefore(params, cont.asCallback(false))
}
private suspend fun loadAfter(params: LoadParams<Key>) =
suspendCancellableCoroutine<BaseResult<Value>> { cont ->
loadAfter(params, cont.asCallback(true))
}
@Suppress("DEPRECATION")
private fun CancellableContinuation<BaseResult<Value>>.asCallback(isAppend: Boolean) =
object : PageKeyedDataSource.LoadCallback<Key, Value>() {
override fun onResult(data: List<Value>, adjacentPageKey: Key?) {
resume(
BaseResult(
data = data,
prevKey = if (isAppend) null else adjacentPageKey,
nextKey = if (isAppend) adjacentPageKey else null
)
)
}
}
/**
* Load initial data.
*
* This method is called first to initialize a PagedList with data. If it's possible to count
* the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
* the callback via the three-parameter [LoadInitialCallback.onResult]. This enables PagedLists
* presenting data from this source to display placeholders to represent unloaded items.
*
* [LoadInitialParams.requestedLoadSize] is a hint, not a requirement, so it may be may be
* altered or ignored.
*
* @param params Parameters for initial load, including requested load size.
* @param callback Callback that receives initial load data.
*/
abstract fun loadInitial(
params: LoadInitialParams<Key>,
callback: LoadInitialCallback<Key, Value>
)
/**
* Prepend page with the key specified by [LoadParams.key].
*
* It's valid to return a different list size than the page size if it's easier, e.g. if your
* backend defines page sizes. It is generally preferred to increase the number loaded than
* reduce.
*
* Data may be passed synchronously during the load method, or deferred and called at a later
* time. Further loads going down will be blocked until the callback is called.
*
* If data cannot be loaded (for example, if the request is invalid, or the data would be stale
* and inconsistent), it is valid to call [invalidate] to invalidate the data source, and
* prevent further loading.
*
* @param params Parameters for the load, including the key for the new page, and requested load
* size.
* @param callback Callback that receives loaded data.
*/
abstract fun loadBefore(params: LoadParams<Key>, callback: LoadCallback<Key, Value>)
/**
* Append page with the key specified by [LoadParams.key].
*
* It's valid to return a different list size than the page size if it's easier, e.g. if your
* backend defines page sizes. It is generally preferred to increase the number loaded than
* reduce.
*
* Data may be passed synchronously during the load method, or deferred and called at a later
* time. Further loads going down will be blocked until the callback is called.
*
* If data cannot be loaded (for example, if the request is invalid, or the data would be stale
* and inconsistent), it is valid to call [invalidate] to invalidate the data source, and
* prevent further loading.
*
* @param params Parameters for the load, including the key for the new page, and requested load
* size.
* @param callback Callback that receives loaded data.
*/
abstract fun loadAfter(params: LoadParams<Key>, callback: LoadCallback<Key, Value>)
@Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly.
internal override fun getKeyInternal(item: Value): Key =
throw IllegalStateException("Cannot get key by item in pageKeyedDataSource")
@Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly.
internal override val supportsPageDropping = false
@Suppress("DEPRECATION")
final override fun <ToValue : Any> mapByPage(
function: Function<List<Value>, List<ToValue>>
): PageKeyedDataSource<Key, ToValue> = WrapperPageKeyedDataSource(this, function)
@Suppress("DEPRECATION")
final override fun <ToValue : Any> mapByPage(
function: (List<Value>) -> List<ToValue>
): PageKeyedDataSource<Key, ToValue> = mapByPage(Function { function(it) })
@Suppress("DEPRECATION")
final override fun <ToValue : Any> map(
function: Function<Value, ToValue>
): PageKeyedDataSource<Key, ToValue> =
mapByPage(Function { list -> list.map { function.apply(it) } })
@Suppress("DEPRECATION")
final override fun <ToValue : Any> map(
function: (Value) -> ToValue
): PageKeyedDataSource<Key, ToValue> = mapByPage(Function { list -> list.map(function) })
}