/*
* 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.IntRange
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.paging.LoadType.REFRESH
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
/**
* @suppress
*/
@Suppress("DEPRECATION")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <Key : Any> PagedList.Config.toRefreshLoadParams(key: Key?): PagingSource.LoadParams<Key> =
PagingSource.LoadParams.Refresh(
key,
initialLoadSizeHint,
enablePlaceholders,
)
/**
* Base class for an abstraction of pageable static data from some source, where loading pages
* of data is typically an expensive operation. Some examples of common [PagingSource]s might be
* from network or from a database.
*
* An instance of a [PagingSource] is used to load pages of data for an instance of [PagingData].
*
* A [PagingData] can grow as it loads more data, but the data loaded cannot be updated. If the
* underlying data set is modified, a new [PagingSource] / [PagingData] pair must be created to
* represent an updated snapshot of the data.
*
* ### Loading Pages
*
* [PagingData] queries data from its [PagingSource] in response to loading hints generated as
* the user scrolls in a `RecyclerView`.
*
* To control how and when a [PagingData] queries data from its [PagingSource], see [PagingConfig],
* which defines behavior such as [PagingConfig.pageSize] and [PagingConfig.prefetchDistance].
*
* ### Updating Data
*
* A [PagingSource] / [PagingData] pair is a snapshot of the data set. A new [PagingData] /
* [PagingData] must be created if an update occurs, such as a reorder, insert, delete, or content
* update occurs. A [PagingSource] must detect that it cannot continue loading its snapshot
* (for instance, when Database query notices a table being invalidated), and call [invalidate].
* Then a new [PagingSource] / [PagingData] pair would be created to represent data from the new
* state of the database query.
*
* ### Presenting Data to UI
*
* To present data loaded by a [PagingSource] to a `RecyclerView`, create an instance of [Pager],
* which provides a stream of [PagingData] that you may collect from and submit to a
* [PagingDataAdapter][androidx.paging.PagingDataAdapter].
*
* @param Key Type of key which define what data to load. E.g. [Int] to represent either a page
* number or item position, or [String] if your network uses Strings as next tokens returned with
* each response.
* @param Value Type of data loaded in by this [PagingSource]. E.g., the type of data that will be
* passed to a [PagingDataAdapter][androidx.paging.PagingDataAdapter] to be displayed in a
* `RecyclerView`.
*
* @sample androidx.paging.samples.pageKeyedPagingSourceSample
* @sample androidx.paging.samples.pageIndexedPagingSourceSample
*
* @see Pager
*/
abstract class PagingSource<Key : Any, Value : Any> {
/**
* Params for a load request on a [PagingSource] from [PagingSource.load].
*/
sealed class LoadParams<Key : Any> constructor(
/**
* Requested number of items to load.
*
* Note: It is valid for [PagingSource.load] to return a [LoadResult] that has a different
* number of items than the requested load size.
*/
val loadSize: Int,
/**
* From [PagingConfig.enablePlaceholders], true if placeholders are enabled and the load
* request for this [LoadParams] should populate [LoadResult.Page.itemsBefore] and
* [LoadResult.Page.itemsAfter] if possible.
*/
val placeholdersEnabled: Boolean,
) {
/**
* Key for the page to be loaded.
*
* [key] can be `null` only if this [LoadParams] is [Refresh], and either no `initialKey`
* is provided to the [Pager] or [PagingSource.getRefreshKey] from the previous
* [PagingSource] returns `null`.
*
* The value of [key] is dependent on the type of [LoadParams]:
* * [Refresh]
* * On initial load, the nullable `initialKey` passed to the [Pager].
* * On subsequent loads due to invalidation or refresh, the result of
* [PagingSource.getRefreshKey].
* * [Prepend] - [LoadResult.Page.prevKey] of the loaded page at the front of the list.
* * [Append] - [LoadResult.Page.nextKey] of the loaded page at the end of the list.
*/
abstract val key: Key?
/**
* Params for an initial load request on a [PagingSource] from [PagingSource.load] or a
* refresh triggered by [invalidate].
*/
class Refresh<Key : Any> constructor(
override val key: Key?,
loadSize: Int,
placeholdersEnabled: Boolean,
) : LoadParams<Key>(
loadSize = loadSize,
placeholdersEnabled = placeholdersEnabled,
)
/**
* Params to load a page of data from a [PagingSource] via [PagingSource.load] to be
* appended to the end of the list.
*/
class Append<Key : Any> constructor(
override val key: Key,
loadSize: Int,
placeholdersEnabled: Boolean,
) : LoadParams<Key>(
loadSize = loadSize,
placeholdersEnabled = placeholdersEnabled,
)
/**
* Params to load a page of data from a [PagingSource] via [PagingSource.load] to be
* prepended to the start of the list.
*/
class Prepend<Key : Any> constructor(
override val key: Key,
loadSize: Int,
placeholdersEnabled: Boolean,
) : LoadParams<Key>(
loadSize = loadSize,
placeholdersEnabled = placeholdersEnabled,
)
internal companion object {
fun <Key : Any> create(
loadType: LoadType,
key: Key?,
loadSize: Int,
placeholdersEnabled: Boolean,
): LoadParams<Key> = when (loadType) {
LoadType.REFRESH -> Refresh(
key = key,
loadSize = loadSize,
placeholdersEnabled = placeholdersEnabled,
)
LoadType.PREPEND -> Prepend(
loadSize = loadSize,
key = requireNotNull(key) {
"key cannot be null for prepend"
},
placeholdersEnabled = placeholdersEnabled,
)
LoadType.APPEND -> Append(
loadSize = loadSize,
key = requireNotNull(key) {
"key cannot be null for append"
},
placeholdersEnabled = placeholdersEnabled,
)
}
}
}
/**
* Result of a load request from [PagingSource.load].
*/
sealed class LoadResult<Key : Any, Value : Any> {
/**
* Error result object for [PagingSource.load].
*
* This return type indicates an expected, recoverable error (such as a network load
* failure). This failure will be forwarded to the UI as a [LoadState.Error], and may be
* retried.
*
* @sample androidx.paging.samples.pageKeyedPagingSourceSample
*/
data class Error<Key : Any, Value : Any>(
val throwable: Throwable
) : LoadResult<Key, Value>()
/**
* Success result object for [PagingSource.load].
*
* @sample androidx.paging.samples.pageKeyedPage
* @sample androidx.paging.samples.pageIndexedPage
*/
data class Page<Key : Any, Value : Any> constructor(
/**
* Loaded data
*/
val data: List<Value>,
/**
* [Key] for previous page if more data can be loaded in that direction, `null`
* otherwise.
*/
val prevKey: Key?,
/**
* [Key] for next page if more data can be loaded in that direction, `null` otherwise.
*/
val nextKey: Key?,
/**
* Optional count of items before the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsBefore: Int = COUNT_UNDEFINED,
/**
* Optional count of items after the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsAfter: Int = COUNT_UNDEFINED
) : LoadResult<Key, Value>() {
/**
* Success result object for [PagingSource.load].
*
* @param data Loaded data
* @param prevKey [Key] for previous page if more data can be loaded in that direction,
* `null` otherwise.
* @param nextKey [Key] for next page if more data can be loaded in that direction,
* `null` otherwise.
*/
constructor(
data: List<Value>,
prevKey: Key?,
nextKey: Key?
) : this(data, prevKey, nextKey, COUNT_UNDEFINED, COUNT_UNDEFINED)
init {
require(itemsBefore == COUNT_UNDEFINED || itemsBefore >= 0) {
"itemsBefore cannot be negative"
}
require(itemsAfter == COUNT_UNDEFINED || itemsAfter >= 0) {
"itemsAfter cannot be negative"
}
}
companion object {
const val COUNT_UNDEFINED = Int.MIN_VALUE
@Suppress("MemberVisibilityCanBePrivate") // Prevent synthetic accessor generation.
internal val EMPTY = Page(emptyList(), null, null, 0, 0)
@Suppress("UNCHECKED_CAST") // Can safely ignore, since the list is empty.
internal fun <Key : Any, Value : Any> empty() = EMPTY as Page<Key, Value>
}
}
}
/**
* `true` if this [PagingSource] supports jumping, `false` otherwise.
*
* Override this to `true` if pseudo-fast scrolling via jumps is supported.
*
* A jump occurs when a `RecyclerView` scrolls through a number of placeholders defined by
* [PagingConfig.jumpThreshold] and triggers a load with [LoadType] [REFRESH].
*
* [PagingSource]s that support jumps should override [getRefreshKey] to return a [Key] that
* would load data fulfilling the viewport given a user's current [PagingState.anchorPosition].
*
* @see [PagingConfig.jumpThreshold]
*/
open val jumpingSupported: Boolean
get() = false
/**
* `true` if this [PagingSource] expects to re-use keys to load distinct pages
* without a call to [invalidate], `false` otherwise.
*/
open val keyReuseSupported: Boolean
get() = false
@VisibleForTesting
internal val onInvalidatedCallbacks = CopyOnWriteArrayList<() -> Unit>()
private val _invalid = AtomicBoolean(false)
/**
* Whether this [PagingSource] has been invalidated, which should happen when the data this
* [PagingSource] represents changes since it was first instantiated.
*/
val invalid: Boolean
get() = _invalid.get()
/**
* Signal the [PagingSource] to stop loading.
*
* This method is idempotent. i.e., If [invalidate] has already been called, subsequent calls to
* this method should have no effect.
*/
fun invalidate() {
if (_invalid.compareAndSet(false, true)) {
onInvalidatedCallbacks.forEach { it.invoke() }
}
}
/**
* Add a callback to invoke when the [PagingSource] is first invalidated.
*
* Once invalidated, a [PagingSource] will not become valid again.
*
* A [PagingSource] will only invoke its callbacks once - the first time [invalidate] is called,
* on that thread.
*
* @param onInvalidatedCallback The callback that will be invoked on thread that invalidates the
* [PagingSource].
*/
fun registerInvalidatedCallback(onInvalidatedCallback: () -> Unit) {
onInvalidatedCallbacks.add(onInvalidatedCallback)
}
/**
* Remove a previously added invalidate callback.
*
* @param onInvalidatedCallback The previously added callback.
*/
fun unregisterInvalidatedCallback(onInvalidatedCallback: () -> Unit) {
onInvalidatedCallbacks.remove(onInvalidatedCallback)
}
/**
* Loading API for [PagingSource].
*
* Implement this method to trigger your async load (e.g. from database or network).
*/
abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
/**
* Provide a [Key] used for the initial [load] for the next [PagingSource] due to invalidation
* of this [PagingSource]. The [Key] is provided to [load] via [LoadParams.key].
*
* The [Key] returned by this method should cause [load] to load enough items to
* fill the viewport around the last accessed position, allowing the next generation to
* transparently animate in. The last accessed position can be retrieved via
* [state.anchorPosition][PagingState.anchorPosition], which is typically
* the top-most or bottom-most item in the viewport due to access being triggered by binding
* items as they scroll into view.
*
* For example, if items are loaded based on integer position keys, you can return
* [state.anchorPosition][PagingState.anchorPosition].
*
* Alternately, if items contain a key used to load, get the key from the item in the page at
* index [state.anchorPosition][PagingState.anchorPosition].
*
* @param state [PagingState] of the currently fetched data, which includes the most recently
* accessed position in the list via [PagingState.anchorPosition].
*
* @return [Key] passed to [load] after invalidation used for initial load of the next
* generation. The [Key] returned by [getRefreshKey] should load pages centered around
* user's current viewport. If the correct [Key] cannot be determined, `null` can be returned
* to allow [load] decide what default key to use.
*/
abstract fun getRefreshKey(state: PagingState<Key, Value>): Key?
}