commonMain.androidx.paging.ContiguousPagedList.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of paging Show documentation
Show all versions of paging Show documentation
Jetpack Paging3 for Kotlin Multiplatform.
/*
* 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.paging.LoadState.Loading
import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadType.APPEND
import androidx.paging.LoadType.PREPEND
import androidx.paging.LoadType.REFRESH
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* @suppress
*/
@Suppress("DEPRECATION")
public open class ContiguousPagedList(
final override val pagingSource: PagingSource,
coroutineScope: CoroutineScope,
notifyDispatcher: CoroutineDispatcher,
backgroundDispatcher: CoroutineDispatcher,
internal val boundaryCallback: BoundaryCallback?,
config: Config,
initialPage: PagingSource.LoadResult.Page,
private val initialLastKey: K?
) : PagedList(
pagingSource,
coroutineScope,
notifyDispatcher,
PagedStorage(),
config,
),
PagedStorage.Callback,
LegacyPageFetcher.PageConsumer {
internal companion object {
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
// if set to true, boundaryCallback is non-null, and should
// be dispatched when nearby load has occurred
private var boundaryCallbackBeginDeferred = false
private var boundaryCallbackEndDeferred = false
// lowest and highest index accessed by loadAround. Used to
// decide when boundaryCallback should be dispatched
private var lowestIndexAccessed = Int.MAX_VALUE
private var highestIndexAccessed = Int.MIN_VALUE
private var replacePagesWithNulls = false
private val shouldTrim = config.maxSize != Config.MAX_SIZE_UNBOUNDED
@Suppress("UNCHECKED_CAST")
private val pager = LegacyPageFetcher(
coroutineScope,
config,
pagingSource,
notifyDispatcher,
backgroundDispatcher,
this,
storage as LegacyPageFetcher.KeyProvider
)
@Suppress("UNCHECKED_CAST")
override val lastKey: K?
get() {
return (storage.getRefreshKeyInfo(config) as PagingState?)
?.let { pagingSource.getRefreshKey(it) }
?: initialLastKey
}
override val isDetached: Boolean
get() = pager.isDetached
/**
* Given a page result, apply or drop it, and return whether more loading is needed.
*/
override fun onPageResult(
type: LoadType,
page: PagingSource.LoadResult.Page<*, V>
): Boolean {
var continueLoading = false
val list = page.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,
list.size
)
if (type == APPEND) {
if (skipNewPage && !trimFromFront) {
// don't append this data, drop it
appendItemsRequested = 0
} else {
storage.appendPage(page, this@ContiguousPagedList)
appendItemsRequested -= list.size
if (appendItemsRequested > 0 && list.isNotEmpty()) {
continueLoading = true
}
}
} else if (type == PREPEND) {
if (skipNewPage && trimFromFront) {
// don't append this data, drop it
prependItemsRequested = 0
} else {
storage.prependPage(page, this@ContiguousPagedList)
prependItemsRequested -= list.size
if (prependItemsRequested > 0 && list.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/ PagingSource) we don't
// allow fetches in same direction - this means reading the load state is safe.
if (trimFromFront) {
if (pager.loadStateManager.startState !is Loading) {
if (storage.trimFromFront(
replacePagesWithNulls,
config.maxSize,
requiredRemainder,
this@ContiguousPagedList
)
) {
// trimmed from front, ensure we can fetch in that dir
pager.loadStateManager.setState(PREPEND, NotLoading.Incomplete)
}
}
} else {
if (pager.loadStateManager.endState !is Loading) {
if (storage.trimFromEnd(
replacePagesWithNulls,
config.maxSize,
requiredRemainder,
this@ContiguousPagedList
)
) {
pager.loadStateManager.setState(APPEND, NotLoading.Incomplete)
}
}
}
}
triggerBoundaryCallback(type, list)
return continueLoading
}
override fun onStateChanged(type: LoadType, state: LoadState) {
dispatchStateChangeAsync(type, state)
}
private fun triggerBoundaryCallback(type: LoadType, page: List) {
if (boundaryCallback != null) {
val deferEmpty = storage.size == 0
val deferBegin = (!deferEmpty && type == PREPEND && page.isEmpty())
val deferEnd = (!deferEmpty && type == APPEND && page.isEmpty())
deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd)
}
}
// Creation thread for initial synchronous load, otherwise main thread
// Safe to access main thread only state - no other thread has reference during construction
internal fun deferBoundaryCallbacks(
deferEmpty: Boolean,
deferBegin: Boolean,
deferEnd: Boolean
) {
if (boundaryCallback == null) {
throw IllegalStateException("Can't defer BoundaryCallback, no instance")
}
/*
* If lowest/highest haven't been initialized, set them to storage size,
* since placeholders must already be computed by this point.
*
* This is just a minor optimization so that BoundaryCallback callbacks are sent immediately
* if the initial load size is smaller than the prefetch window (see
* TiledPagedListTest#boundaryCallback_immediate())
*/
if (lowestIndexAccessed == Int.MAX_VALUE) {
lowestIndexAccessed = storage.size
}
if (highestIndexAccessed == Int.MIN_VALUE) {
highestIndexAccessed = 0
}
if (deferEmpty || deferBegin || deferEnd) {
// Post to the main thread, since we may be on creation thread currently
coroutineScope.launch(notifyDispatcher) {
// on is dispatched immediately, since items won't be accessed
if (deferEmpty) {
boundaryCallback.onZeroItemsLoaded()
}
// for other callbacks, mark deferred, and only dispatch if loadAround
// has been called near to the position
if (deferBegin) {
boundaryCallbackBeginDeferred = true
}
if (deferEnd) {
boundaryCallbackEndDeferred = true
}
tryDispatchBoundaryCallbacks(false)
}
}
}
/**
* Call this when lowest/HighestIndexAccessed are changed, or boundaryCallbackBegin/EndDeferred
* is set.
*/
private fun tryDispatchBoundaryCallbacks(post: Boolean) {
val dispatchBegin = boundaryCallbackBeginDeferred &&
lowestIndexAccessed <= config.prefetchDistance
val dispatchEnd = boundaryCallbackEndDeferred &&
highestIndexAccessed >= size - 1 - config.prefetchDistance
if (!dispatchBegin && !dispatchEnd) return
if (dispatchBegin) {
boundaryCallbackBeginDeferred = false
}
if (dispatchEnd) {
boundaryCallbackEndDeferred = false
}
if (post) {
coroutineScope.launch(notifyDispatcher) {
dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd)
}
} else {
dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd)
}
}
private fun dispatchBoundaryCallbacks(begin: Boolean, end: Boolean) {
// safe to deref boundaryCallback here, since we only defer if boundaryCallback present
if (begin) {
boundaryCallback!!.onItemAtFrontLoaded(storage.firstLoadedItem)
}
if (end) {
boundaryCallback!!.onItemAtEndLoaded(storage.lastLoadedItem)
}
}
override fun retry() {
super.retry()
pager.retry()
pager.loadStateManager.refreshState.run {
// If loading the next PagedList failed, signal the retry callback.
if (this is LoadState.Error) {
refreshRetryCallback?.run()
}
}
}
init {
if (config.enablePlaceholders) {
// Placeholders enabled, pass raw data to storage init
storage.init(
if (initialPage.itemsBefore != COUNT_UNDEFINED) initialPage.itemsBefore else 0,
initialPage,
if (initialPage.itemsAfter != COUNT_UNDEFINED) initialPage.itemsAfter else 0,
0,
this,
initialPage.itemsBefore != COUNT_UNDEFINED &&
initialPage.itemsAfter != COUNT_UNDEFINED
)
} else {
// If placeholder are disabled, avoid passing leading/trailing nulls, since PagingSource
// may have passed them anyway.
storage.init(
0,
initialPage,
0,
if (initialPage.itemsBefore != COUNT_UNDEFINED) initialPage.itemsBefore else 0,
this,
false
)
}
triggerBoundaryCallback(REFRESH, initialPage.data)
}
override fun dispatchCurrentLoadState(callback: (LoadType, LoadState) -> Unit) {
pager.loadStateManager.dispatchCurrentLoadState(callback)
}
override fun setInitialLoadState(loadType: LoadType, loadState: LoadState) {
pager.loadStateManager.setState(loadType, loadState)
}
override fun loadAroundInternal(index: Int) {
val prependItems =
getPrependItemsRequested(config.prefetchDistance, index, storage.placeholdersBefore)
val appendItems = getAppendItemsRequested(
config.prefetchDistance,
index,
storage.placeholdersBefore + storage.storageCount
)
prependItemsRequested = maxOf(prependItems, prependItemsRequested)
if (prependItemsRequested > 0) {
pager.trySchedulePrepend()
}
appendItemsRequested = maxOf(appendItems, appendItemsRequested)
if (appendItemsRequested > 0) {
pager.tryScheduleAppend()
}
lowestIndexAccessed = minOf(lowestIndexAccessed, index)
highestIndexAccessed = maxOf(highestIndexAccessed, index)
/*
* lowestIndexAccessed / highestIndexAccessed have been updated, so check if we need to
* dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
* and accesses happen near the boundaries.
*
* Note: we post here, since RecyclerView may want to add items in response, and this
* call occurs in PagedListAdapter bind.
*/
tryDispatchBoundaryCallbacks(true)
}
override fun detach(): Unit = pager.detach()
override fun onInitialized(count: Int) {
notifyInserted(0, count)
// Simple heuristic to decide if, when dropping pages, we should replace with placeholders.
// If we're not presenting placeholders at initialization time, we won't add them when
// we drop a page. Note that we don't use config.enablePlaceholders, since the
// PagingSource may have opted not to load any.
replacePagesWithNulls = storage.placeholdersBefore > 0 ||
storage.placeholdersAfter > 0
}
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)
// update access range
lowestIndexAccessed += added
highestIndexAccessed += added
}
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)
}
override fun onPagesRemoved(startOfDrops: Int, count: Int) {
notifyRemoved(startOfDrops, count)
}
override fun onPagesSwappedToPlaceholder(startOfDrops: Int, count: Int) {
notifyChanged(startOfDrops, count)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy