blob: 223d698bca1e841886e911a4c4cfc69d2c99753b [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.MainThread
import androidx.annotation.RestrictTo
import androidx.paging.PagedSource.KeyProvider
import androidx.paging.PagedSource.LoadResult.Companion.COUNT_UNDEFINED
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
open class ContiguousPagedList<K : Any, V : Any>(
pagedSource: PagedSource<K, V>,
coroutineScope: CoroutineScope,
notifyDispatcher: CoroutineDispatcher,
backgroundDispatcher: CoroutineDispatcher,
boundaryCallback: BoundaryCallback<V>?,
config: Config,
initialResult: PagedSource.LoadResult<K, V>,
lastLoad: Int
) : PagedList<V>(
coroutineScope,
pagedSource,
PagedStorage<V>(),
notifyDispatcher,
backgroundDispatcher,
boundaryCallback,
config
), PagedStorage.Callback, Pager.PageConsumer<V> {
internal companion object {
internal const val LAST_LOAD_UNSPECIFIED = -1
internal fun getPrependItemsRequested(
prefetchDistance: Int,
index: Int,
leadingNulls: Int
) = prefetchDistance - (index - leadingNulls)
internal fun getAppendItemsRequested(
prefetchDistance: Int,
index: Int,
itemsBeforeTrailingNulls: Int
) = index + prefetchDistance + 1 - itemsBeforeTrailingNulls
}
private var prependItemsRequested = 0
private var appendItemsRequested = 0
private var replacePagesWithNulls = false
private val shouldTrim = (pagedSource.keyProvider is KeyProvider.Positional ||
pagedSource.keyProvider is KeyProvider.ItemKey) &&
config.maxSize != Config.MAX_SIZE_UNBOUNDED
private val pager = Pager(
coroutineScope,
config,
pagedSource,
notifyDispatcher,
backgroundDispatcher,
this,
initialResult,
storage
)
override val isDetached
get() = pager.isDetached
override val lastKey
get() = when (val keyProvider = pagedSource.keyProvider) {
is KeyProvider.Positional -> {
@Suppress("UNCHECKED_CAST")
lastLoad as K
}
is KeyProvider.PageKey ->
throw IllegalStateException("Cannot get key by item from KeyProvider.PageKey")
is KeyProvider.ItemKey -> lastItem?.let { keyProvider.getKey(it) }
}
/**
* Given a page result, apply or drop it, and return whether more loading is needed.
*/
override fun onPageResult(type: LoadType, pageResult: PagedSource.LoadResult<*, V>): Boolean {
var continueLoading = false
val page = pageResult.data
// if we end up trimming, we trim from side that's furthest from most recent access
val trimFromFront = lastLoad > storage.middleOfLoadedRange
// is the new page big enough to warrant pre-trimming (i.e. dropping) it?
val skipNewPage = shouldTrim && storage.shouldPreTrimNewPage(
config.maxSize,
requiredRemainder,
page.size
)
if (type == LoadType.END) {
if (skipNewPage && !trimFromFront) {
// don't append this data, drop it
appendItemsRequested = 0
} else {
storage.appendPage(page, this@ContiguousPagedList)
appendItemsRequested -= page.size
if (appendItemsRequested > 0 && page.isNotEmpty()) {
continueLoading = true
}
}
} else if (type == LoadType.START) {
if (skipNewPage && trimFromFront) {
// don't append this data, drop it
prependItemsRequested = 0
} else {
storage.prependPage(page, this@ContiguousPagedList)
prependItemsRequested -= page.size
if (prependItemsRequested > 0 && page.isNotEmpty()) {
continueLoading = true
}
}
} else {
throw IllegalArgumentException("unexpected result type $type")
}
if (shouldTrim) {
// Try and trim, but only if the side being trimmed isn't actually fetching.
// For simplicity (both of impl here, and contract w/ PagedSource) we don't
// allow fetches in same direction - this means reading the load state is safe.
if (trimFromFront) {
if (pager.loadStateManager.start != LoadState.LOADING) {
if (storage.trimFromFront(
replacePagesWithNulls,
config.maxSize,
requiredRemainder,
this@ContiguousPagedList
)
) {
// trimmed from front, ensure we can fetch in that dir
pager.loadStateManager.setState(
LoadType.START,
LoadState.IDLE,
null
)
}
}
} else {
if (pager.loadStateManager.end != LoadState.LOADING) {
if (storage.trimFromEnd(
replacePagesWithNulls,
config.maxSize,
requiredRemainder,
this@ContiguousPagedList
)
) {
pager.loadStateManager.setState(LoadType.END, LoadState.IDLE, null)
}
}
}
}
triggerBoundaryCallback(type, page)
return continueLoading
}
override fun onStateChanged(type: LoadType, state: LoadState, error: Throwable?) =
dispatchStateChange(type, state, error)
private fun triggerBoundaryCallback(type: LoadType, page: List<V>) {
if (boundaryCallback != null) {
val deferEmpty = storage.size == 0
val deferBegin = (!deferEmpty && type == LoadType.START && page.isEmpty())
val deferEnd = (!deferEmpty && type == LoadType.END && page.isEmpty())
deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd)
}
}
override fun retry() {
super.retry()
pager.retry()
if (pager.loadStateManager.refresh == LoadState.RETRYABLE_ERROR) {
// Loading the next PagedList failed, signal the retry callback.
refreshRetryCallback?.run()
}
}
init {
this.lastLoad = lastLoad
if (config.enablePlaceholders) {
// Placeholders enabled, pass raw data to storage init
storage.init(
if (initialResult.itemsBefore != COUNT_UNDEFINED) initialResult.itemsBefore else 0,
initialResult.data,
if (initialResult.itemsAfter != COUNT_UNDEFINED) initialResult.itemsAfter else 0,
initialResult.offset,
this
)
} else {
// If placeholder are disabled, avoid passing leading/trailing nulls, since PagedSource
// may have passed them anyway.
storage.init(
0,
initialResult.data,
0,
initialResult.itemsBefore + initialResult.offset,
this
)
}
if (this.lastLoad == LAST_LOAD_UNSPECIFIED) {
// Because the ContiguousPagedList wasn't initialized with a last load position,
// initialize it to the middle of the initial load
val itemsBefore =
if (initialResult.itemsBefore != COUNT_UNDEFINED) initialResult.itemsBefore else 0
this.lastLoad = itemsBefore + initialResult.data.size / 2
}
triggerBoundaryCallback(LoadType.REFRESH, initialResult.data)
}
override fun dispatchCurrentLoadState(callback: LoadStateListener) =
pager.loadStateManager.dispatchCurrentLoadState(callback)
override fun setInitialLoadState(loadState: LoadState, error: Throwable?) =
pager.loadStateManager.setState(LoadType.REFRESH, loadState, error)
@MainThread
override fun dispatchUpdatesSinceSnapshot(snapshot: PagedList<V>, callback: Callback) {
val snapshotStorage = snapshot.storage
val newlyAppended = storage.numberAppended - snapshotStorage.numberAppended
val newlyPrepended = storage.numberPrepended - snapshotStorage.numberPrepended
val previousTrailing = snapshotStorage.trailingNullCount
val previousLeading = snapshotStorage.leadingNullCount
// Validate that the snapshot looks like a previous version of this list - if it's not,
// we can't be sure we'll dispatch callbacks safely
if (snapshotStorage.isEmpty() ||
newlyAppended < 0 ||
newlyPrepended < 0 ||
storage.trailingNullCount != maxOf(previousTrailing - newlyAppended, 0) ||
storage.leadingNullCount != maxOf(previousLeading - newlyPrepended, 0) ||
storage.storageCount != snapshotStorage.storageCount + newlyAppended + newlyPrepended
) {
throw IllegalArgumentException(
"Invalid snapshot provided - doesn't appear to be a snapshot of this PagedList"
)
}
if (newlyAppended != 0) {
val changedCount = minOf(previousTrailing, newlyAppended)
val addedCount = newlyAppended - changedCount
val endPosition = snapshotStorage.leadingNullCount + snapshotStorage.storageCount
if (changedCount != 0) {
callback.onChanged(endPosition, changedCount)
}
if (addedCount != 0) {
callback.onInserted(endPosition + changedCount, addedCount)
}
}
if (newlyPrepended != 0) {
val changedCount = minOf(previousLeading, newlyPrepended)
val addedCount = newlyPrepended - changedCount
if (changedCount != 0) {
callback.onChanged(previousLeading, changedCount)
}
if (addedCount != 0) {
callback.onInserted(0, addedCount)
}
}
}
@MainThread
override fun loadAroundInternal(index: Int) {
val prependItems =
getPrependItemsRequested(config.prefetchDistance, index, storage.leadingNullCount)
val appendItems = getAppendItemsRequested(
config.prefetchDistance,
index,
storage.leadingNullCount + storage.storageCount
)
prependItemsRequested = maxOf(prependItems, prependItemsRequested)
if (prependItemsRequested > 0) {
pager.trySchedulePrepend()
}
appendItemsRequested = maxOf(appendItems, appendItemsRequested)
if (appendItemsRequested > 0) {
pager.tryScheduleAppend()
}
}
override fun detach() = pager.detach()
@MainThread
override fun onInitialized(count: Int) {
notifyInserted(0, count)
// simple heuristic to decide if, when dropping pages, we should replace with placeholders
replacePagesWithNulls = storage.leadingNullCount > 0 || storage.trailingNullCount > 0
}
@MainThread
override fun onPagePrepended(leadingNulls: Int, changed: Int, added: Int) {
// finally dispatch callbacks, after prepend may have already been scheduled
notifyChanged(leadingNulls, changed)
notifyInserted(0, added)
offsetAccessIndices(added)
}
@MainThread
override fun onPageAppended(endPosition: Int, changed: Int, added: Int) {
// finally dispatch callbacks, after append may have already been scheduled
notifyChanged(endPosition, changed)
notifyInserted(endPosition + changed, added)
}
@MainThread
override fun onPagePlaceholderInserted(pageIndex: Int) {
throw IllegalStateException("Tiled callback on ContiguousPagedList")
}
@MainThread
override fun onPageInserted(start: Int, count: Int) {
throw IllegalStateException("Tiled callback on ContiguousPagedList")
}
override fun onPagesRemoved(startOfDrops: Int, count: Int) = notifyRemoved(startOfDrops, count)
override fun onPagesSwappedToPlaceholder(startOfDrops: Int, count: Int) =
notifyChanged(startOfDrops, count)
}