blob: 61c1f027083b56bae1f6ca4fde1844ab167291f1 [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.IntRange
import androidx.annotation.RestrictTo
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(
LoadType.REFRESH,
key,
initialLoadSizeHint,
enablePlaceholders,
pageSize
)
/**
* 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.
*
* @param Key Type for unique identifier for items loaded from [PagingSource]. E.g., [Int] to
* represent an item's position in a [PagingSource] that is keyed by item position. Note that this
* is distinct from e.g. Room's `<Value>` type loaded by the [PagingSource].
* @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.pagingSourceSample
*/
@Suppress("KDocUnresolvedReference")
abstract class PagingSource<Key : Any, Value : Any> {
/**
* Params for a load request on a [PagingSource] from [PagingSource.load].
*/
data class LoadParams<Key : Any>(
/**
* [LoadType] of this load request.
*
* May be one of the following values:
* * [LoadType.REFRESH] - initial load or a refresh triggered by [invalidate].
* * [LoadType.START] - load a page of data to be prepended to the start of the list.
* * [LoadType.END] - load a page of data to be appended to the end of the list.
*/
val loadType: LoadType,
/**
* Key for the page to be loaded.
*/
val key: Key?,
/**
* 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,
/**
* From [PagingConfig.pageSize], the configured page size.
*/
val pageSize: Int
)
/**
* Result of a load request from [PagingSource.load].
*/
sealed class LoadResult<Key : Any, Value : Any> {
/**
* Recoverable error used to update [LoadState] in a [PagingData] that may be retried via
* [PagingDataDiffer.retry].
*/
data class Error<Key : Any, Value : Any>(
val throwable: Throwable
) : LoadResult<Key, Value>()
/**
* Success result object for [PagingSource.load].
*/
data class Page<Key : Any, Value : Any>(
/**
* 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>() {
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.
private 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
/**
* Request a refresh key given the current [PagingState] of the associated [PagingData] used to
* present loaded data from this [PagingSource].
*
* The [Key] returned by this method is used to populate the [LoadParams.key] for load requests
* of type [LoadType.REFRESH].
*
* For example, if items are loaded based on position, and keys are positions, [getRefreshKey]
* should return the position of the item.
*
* Alternately, if items contain a key used to load, get the key from the item in the page at
* index [PagingState.anchorPosition].
*
* If this operation cannot be supported (generally, because keys cannot be reused across
* refresh) return `null` - this is the default behavior.
*
* Note: This method is guaranteed to only be called if the initial load succeeds and the
* list of loaded pages is not empty. In the case where a refresh is triggered before the
* initial load succeeds or it errors out, the initial key passed into the creation of the
* [PagingData] stream (typically from [PagingDataFlow]) will be used.
*/
open fun getRefreshKey(state: PagingState<Key, Value>): Key? = null
private 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.
*/
open fun invalidate() {
// TODO(b/137971356): Investigate making this not open when able to remove
// LegacyPagingSource.
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>
}