/*
* 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.LoadType.APPEND
import androidx.paging.LoadType.PREPEND
import androidx.paging.LoadType.REFRESH
import androidx.paging.internal.appendMediatorStatesIfNotNull
/**
* Events in the stream from paging fetch logic to UI.
*
* Every event sent to the UI is a PageEvent, and will be processed atomically.
*/
internal sealed class PageEvent<T : Any> {
/**
* Represents a fully-terminal, static list of data.
*
* This event should always be the first and only emission in a Flow<PageEvent> within a
* generation.
*
* @param sourceLoadStates source [LoadStates] to emit if non-null, ignored otherwise, allowing
* the presenter receiving this event to maintain the previous state.
* @param mediatorLoadStates mediator [LoadStates] to emit if non-null, ignored otherwise,
* allowing the presenter receiving this event to maintain its previous state.
*/
data class StaticList<T : Any>(
val data: List<T>,
val sourceLoadStates: LoadStates? = null,
val mediatorLoadStates: LoadStates? = null
) : PageEvent<T>() {
override suspend fun <R : Any> map(transform: suspend (T) -> R): PageEvent<R> {
return StaticList(
data = data.map { transform(it) },
sourceLoadStates = sourceLoadStates,
mediatorLoadStates = mediatorLoadStates,
)
}
override suspend fun <R : Any> flatMap(
transform: suspend (T) -> Iterable<R>
): PageEvent<R> {
return StaticList(
data = data.flatMap { transform(it) },
sourceLoadStates = sourceLoadStates,
mediatorLoadStates = mediatorLoadStates,
)
}
override suspend fun filter(predicate: suspend (T) -> Boolean): PageEvent<T> {
return StaticList(
data = data.filter { predicate(it) },
sourceLoadStates = sourceLoadStates,
mediatorLoadStates = mediatorLoadStates,
)
}
override fun toString(): String {
return appendMediatorStatesIfNotNull(mediatorLoadStates) {
"""PageEvent.StaticList with ${data.size} items (
| first item: ${data.firstOrNull()}
| last item: ${data.lastOrNull()}
| sourceLoadStates: $sourceLoadStates
"""
}
}
}
// Intentional to prefer Refresh, Prepend, Append constructors from Companion.
@Suppress("DataClassPrivateConstructor")
data class Insert<T : Any> private constructor(
val loadType: LoadType,
val pages: List<TransformablePage<T>>,
val placeholdersBefore: Int,
val placeholdersAfter: Int,
val sourceLoadStates: LoadStates,
val mediatorLoadStates: LoadStates? = null
) : PageEvent<T>() {
init {
require(loadType == APPEND || placeholdersBefore >= 0) {
"Prepend insert defining placeholdersBefore must be > 0, but was" +
" $placeholdersBefore"
}
require(loadType == PREPEND || placeholdersAfter >= 0) {
"Append insert defining placeholdersAfter must be > 0, but was" +
" $placeholdersAfter"
}
require(loadType != REFRESH || pages.isNotEmpty()) {
"Cannot create a REFRESH Insert event with no TransformablePages as this could " +
"permanently stall pagination. Note that this check does not prevent empty " +
"LoadResults and is instead usually an indication of an internal error in " +
"Paging itself."
}
}
private inline fun <R : Any> mapPages(
transform: (TransformablePage<T>) -> TransformablePage<R>
) = transformPages { it.map(transform) }
internal inline fun <R : Any> transformPages(
transform: (List<TransformablePage<T>>) -> List<TransformablePage<R>>
): Insert<R> = Insert(
loadType = loadType,
pages = transform(pages),
placeholdersBefore = placeholdersBefore,
placeholdersAfter = placeholdersAfter,
sourceLoadStates = sourceLoadStates,
mediatorLoadStates = mediatorLoadStates,
)
override suspend fun <R : Any> map(transform: suspend (T) -> R): PageEvent<R> = mapPages {
TransformablePage(
originalPageOffsets = it.originalPageOffsets,
data = it.data.map { item -> transform(item) },
hintOriginalPageOffset = it.hintOriginalPageOffset,
hintOriginalIndices = it.hintOriginalIndices
)
}
override suspend fun <R : Any> flatMap(
transform: suspend (T) -> Iterable<R>
): PageEvent<R> = mapPages {
val data = mutableListOf<R>()
val originalIndices = mutableListOf<Int>()
it.data.forEachIndexed { index, t ->
data += transform(t)
val indexToStore = it.hintOriginalIndices?.get(index) ?: index
while (originalIndices.size < data.size) {
originalIndices.add(indexToStore)
}
}
TransformablePage(
originalPageOffsets = it.originalPageOffsets,
data = data,
hintOriginalPageOffset = it.hintOriginalPageOffset,
hintOriginalIndices = originalIndices
)
}
override suspend fun filter(predicate: suspend (T) -> Boolean): PageEvent<T> = mapPages {
val data = mutableListOf<T>()
val originalIndices = mutableListOf<Int>()
it.data.forEachIndexed { index, t ->
if (predicate(t)) {
data.add(t)
originalIndices.add(it.hintOriginalIndices?.get(index) ?: index)
}
}
TransformablePage(
originalPageOffsets = it.originalPageOffsets,
data = data,
hintOriginalPageOffset = it.hintOriginalPageOffset,
hintOriginalIndices = originalIndices
)
}
companion object {
fun <T : Any> Refresh(
pages: List<TransformablePage<T>>,
placeholdersBefore: Int,
placeholdersAfter: Int,
sourceLoadStates: LoadStates,
mediatorLoadStates: LoadStates? = null
) = Insert(
REFRESH,
pages,
placeholdersBefore,
placeholdersAfter,
sourceLoadStates,
mediatorLoadStates,
)
fun <T : Any> Prepend(
pages: List<TransformablePage<T>>,
placeholdersBefore: Int,
sourceLoadStates: LoadStates,
mediatorLoadStates: LoadStates? = null
) = Insert(
PREPEND,
pages,
placeholdersBefore,
-1,
sourceLoadStates,
mediatorLoadStates,
)
fun <T : Any> Append(
pages: List<TransformablePage<T>>,
placeholdersAfter: Int,
sourceLoadStates: LoadStates,
mediatorLoadStates: LoadStates? = null
) = Insert(
APPEND,
pages,
-1,
placeholdersAfter,
sourceLoadStates,
mediatorLoadStates,
)
/**
* Empty refresh, used to convey initial state.
*
* Note - has no remote state, so remote state may be added over time
*/
val EMPTY_REFRESH_LOCAL: Insert<Any> = Refresh(
pages = listOf(TransformablePage.EMPTY_INITIAL_PAGE),
placeholdersBefore = 0,
placeholdersAfter = 0,
sourceLoadStates = LoadStates(
refresh = LoadState.NotLoading.Incomplete,
prepend = LoadState.NotLoading.Complete,
append = LoadState.NotLoading.Complete,
),
)
}
override fun toString(): String {
val itemCount = pages.fold(0) { total, page -> total + page.data.size }
val placeholdersBefore = if (placeholdersBefore != -1) "$placeholdersBefore" else "none"
val placeholdersAfter = if (placeholdersAfter != -1) "$placeholdersAfter" else "none"
return appendMediatorStatesIfNotNull(mediatorLoadStates) {
"""PageEvent.Insert for $loadType, with $itemCount items (
| first item: ${pages.firstOrNull()?.data?.firstOrNull()}
| last item: ${pages.lastOrNull()?.data?.lastOrNull()}
| placeholdersBefore: $placeholdersBefore
| placeholdersAfter: $placeholdersAfter
| sourceLoadStates: $sourceLoadStates
"""
}
}
}
// TODO: b/195658070 consider refactoring Drop events to carry full source/mediator states.
data class Drop<T : Any>(
val loadType: LoadType,
/**
* Smallest [TransformablePage.originalPageOffsets] to drop; inclusive.
*/
val minPageOffset: Int,
/**
* Largest [TransformablePage.originalPageOffsets] to drop; inclusive
*/
val maxPageOffset: Int,
val placeholdersRemaining: Int
) : PageEvent<T>() {
init {
require(loadType != REFRESH) { "Drop load type must be PREPEND or APPEND" }
require(pageCount > 0) { "Drop count must be > 0, but was $pageCount" }
require(placeholdersRemaining >= 0) {
"Invalid placeholdersRemaining $placeholdersRemaining"
}
}
val pageCount get() = maxPageOffset - minPageOffset + 1
override fun toString(): String {
val direction = when (loadType) {
APPEND -> "end"
PREPEND -> "front"
else -> throw IllegalArgumentException(
"Drop load type must be PREPEND or APPEND"
)
}
return """PageEvent.Drop from the $direction (
| minPageOffset: $minPageOffset
| maxPageOffset: $maxPageOffset
| placeholdersRemaining: $placeholdersRemaining
|)""".trimMargin()
}
}
/**
* A [PageEvent] to notify presenter layer of changes in local and remote LoadState.
*
* Uses two LoadStates objects instead of CombinedLoadStates so that consumers like
* PagingDataDiffer can define behavior of convenience properties
*/
data class LoadStateUpdate<T : Any>(
val source: LoadStates,
val mediator: LoadStates? = null,
) : PageEvent<T>() {
override fun toString(): String {
return appendMediatorStatesIfNotNull(mediator) {
"""PageEvent.LoadStateUpdate (
| sourceLoadStates: $source
"""
}
}
}
@Suppress("UNCHECKED_CAST")
open suspend fun <R : Any> map(transform: suspend (T) -> R): PageEvent<R> = this as PageEvent<R>
@Suppress("UNCHECKED_CAST")
open suspend fun <R : Any> flatMap(transform: suspend (T) -> Iterable<R>): PageEvent<R> {
return this as PageEvent<R>
}
open suspend fun filter(predicate: suspend (T) -> Boolean): PageEvent<T> = this
}