Updated on 2026-08-14
This commit is contained in:
commit
e2403d5f5f
15 changed files with 698 additions and 0 deletions
1
core/pagination/.gitignore
vendored
Normal file
1
core/pagination/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
10
core/pagination/build.gradle.kts
Normal file
10
core/pagination/build.gradle.kts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// region Coroutines
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
}
|
||||
13
core/pagination/src/main/java/com/tangem/pagination/Batch.kt
Normal file
13
core/pagination/src/main/java/com/tangem/pagination/Batch.kt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
/**
|
||||
* Represents a batch of data with a key.
|
||||
* Used in [BatchListState].
|
||||
*
|
||||
* @param TKey type of the key.
|
||||
* @param TData type of the data.
|
||||
*/
|
||||
data class Batch<TKey, TData>(
|
||||
val key: TKey,
|
||||
val data: TData,
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
/**
|
||||
* Action that can be dispatched to [BatchListSource].
|
||||
*
|
||||
* @param TRequestParams type of the request params to load batches.
|
||||
* @param TKey type of the key of the batch.
|
||||
* @param TUpdate type of the update request.
|
||||
*/
|
||||
sealed class BatchAction<TKey, TRequestParams, TUpdate> {
|
||||
|
||||
/**
|
||||
* Action to load the first batch.
|
||||
*
|
||||
* @param requestParams request params to load the first batch.
|
||||
*/
|
||||
data class Reload<TRequestParams : Any>(
|
||||
val requestParams: TRequestParams,
|
||||
) : BatchAction<Nothing, TRequestParams, Nothing>()
|
||||
|
||||
/**
|
||||
* Action to load the next batch.
|
||||
*
|
||||
* @param requestParams request params to load the next batch with new request.
|
||||
* If null, the last request will be used.
|
||||
* Will be saved in the state and used for future LoadMore actions with request = null.
|
||||
*/
|
||||
data class LoadMore<TRequestParams : Any>(
|
||||
val requestParams: TRequestParams? = null,
|
||||
) : BatchAction<Nothing, TRequestParams, Nothing>()
|
||||
|
||||
/**
|
||||
* Action to update the batch.
|
||||
*
|
||||
* @param keys keys of the batches to update.
|
||||
* @param updateRequest request to update the batches.
|
||||
*/
|
||||
class UpdateBatches<TKey, TUpdate>(
|
||||
val keys: Set<TKey>,
|
||||
val updateRequest: TUpdate,
|
||||
) : BatchAction<TKey, Nothing, TUpdate>()
|
||||
|
||||
/**
|
||||
* Action to cancel the current batch loading.
|
||||
*/
|
||||
data object CancelBatchLoading : BatchAction<Nothing, Nothing, Nothing>()
|
||||
|
||||
/**
|
||||
* Action to cancel all update requests.
|
||||
*/
|
||||
data object CancelAllUpdates : BatchAction<Nothing, Nothing, Nothing>()
|
||||
|
||||
/**
|
||||
* Action to cancel update requests that satisfy the predicate.
|
||||
*
|
||||
* @param predicate predicate to check if the update request should be cancelled.
|
||||
*/
|
||||
class CancelUpdates<TKey, TUpdate>(
|
||||
val predicate: (UpdateBatches<TKey, TUpdate>) -> Boolean,
|
||||
) : BatchAction<TKey, Nothing, TUpdate>()
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
/**
|
||||
* Represents a result of a batch fetch request.
|
||||
* Used in [BatchListState].
|
||||
*
|
||||
* @param TData type of the data.
|
||||
*/
|
||||
sealed class BatchFetchResult<out TData> {
|
||||
|
||||
/**
|
||||
* Represents a successful result of a batch fetch request.
|
||||
*
|
||||
* @param data fetched data.
|
||||
* @param last indicates if this is the last batch for the request.
|
||||
*/
|
||||
data class Success<TData>(
|
||||
val data: TData,
|
||||
val last: Boolean = false,
|
||||
) : BatchFetchResult<TData>()
|
||||
|
||||
/**
|
||||
* Represents an error result of a batch fetch request.
|
||||
* Also used for unexpected exceptions that occurred in fetch method in BatchFetcher.
|
||||
*
|
||||
* @param throwable throwable that occurred during the request.
|
||||
* @see com.tangem.pagination.fetcher.BatchFetcher
|
||||
*/
|
||||
class Error(val throwable: Throwable) : BatchFetchResult<Nothing>()
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
import com.tangem.pagination.fetcher.BatchFetcher
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Source for paginated data. The starting point for pagination.
|
||||
*
|
||||
* @param TKey Type of the key that identifies a batch.
|
||||
* @param TData Type of the data in the batch. Generally, it' a list of items.
|
||||
* @param TUpdate Type of the update request.
|
||||
* @property state State of the paginated data.
|
||||
* @property updateResults Flow of results of update requests.
|
||||
*/
|
||||
interface BatchListSource<TKey, TData, TUpdate> {
|
||||
val state: StateFlow<BatchListState<TKey, TData>>
|
||||
val updateResults: SharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new [BatchListSource] with the provided configuration.
|
||||
*
|
||||
* @param fetchDispatcher Dispatcher for fetch operations.
|
||||
* @param context Context for batching.
|
||||
* @param generateNewKey Function to generate a new key for a batch.
|
||||
* @param batchFetcher Function to fetch a batch of data.
|
||||
*
|
||||
* @return New instance of [BatchListSource].
|
||||
*/
|
||||
@Suppress("FunctionNaming")
|
||||
fun <TKey, TData, TRequestParams : Any> BatchListSource(
|
||||
fetchDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
context: BatchingContext<TKey, TRequestParams, Nothing>,
|
||||
generateNewKey: suspend (List<TKey>) -> TKey,
|
||||
batchFetcher: BatchFetcher<TRequestParams, TData>,
|
||||
): BatchListSource<TKey, TData, Nothing> =
|
||||
DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null)
|
||||
|
||||
/**
|
||||
* Creates a new [BatchListSource] with the provided configuration.
|
||||
*
|
||||
* @param fetchDispatcher Dispatcher for fetch operations.
|
||||
* @param context Context for batching.
|
||||
* @param generateNewKey Function to generate a new key for a batch.
|
||||
* @param batchFetcher Function to fetch a batch of data.
|
||||
* @param updateFetcher Function to fetch updates for batches.
|
||||
*
|
||||
* @return New instance of [BatchListSource].
|
||||
*/
|
||||
@Suppress("FunctionNaming")
|
||||
fun <TKey, TData, TRequestParams : Any, TUpdate> BatchListSource(
|
||||
fetchDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
context: BatchingContext<TKey, TRequestParams, TUpdate>,
|
||||
generateNewKey: suspend (List<TKey>) -> TKey,
|
||||
batchFetcher: BatchFetcher<TRequestParams, TData>,
|
||||
updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>,
|
||||
): BatchListSource<TKey, TData, TUpdate> =
|
||||
DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher)
|
||||
|
||||
private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>(
|
||||
private val fetchDispatcher: CoroutineDispatcher,
|
||||
private val context: BatchingContext<TKey, TRequestParams, TUpdate>,
|
||||
private val generateNewKey: suspend (List<TKey>) -> TKey,
|
||||
private val batchFetcher: BatchFetcher<TRequestParams, TData>,
|
||||
private val updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>? = null,
|
||||
) : BatchListSource<TKey, TData, TUpdate> {
|
||||
|
||||
override val state = MutableStateFlow(BatchListState<TKey, TData>(emptyList(), PaginationStatus.None))
|
||||
override val updateResults = MutableSharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
private val scope = context.coroutineScope
|
||||
private val updateJobs = MutableStateFlow<List<Pair<BatchAction.UpdateBatches<TKey, TUpdate>, Job>>>(emptyList())
|
||||
private val waitingUpdateJobs =
|
||||
MutableStateFlow<List<Pair<BatchAction.UpdateBatches<TKey, TUpdate>, Job>>>(emptyList())
|
||||
|
||||
private val lastRequestResult = MutableStateFlow<BatchFetchResult<TData>?>(null)
|
||||
private var reloadActionJob: Job? = null
|
||||
private var loadMoreActionJob: Job? = null
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
try {
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
loadMoreActionJob = null
|
||||
loadMoreActionJob = null
|
||||
lastRequestResult.value = null
|
||||
stopAllUpdates()
|
||||
state.value = BatchListState(emptyList(), PaginationStatus.None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
context.actionsFlow.collect { action ->
|
||||
collectActions(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectActions(action: BatchAction<TKey, TRequestParams, TUpdate>) {
|
||||
when (action) {
|
||||
is BatchAction.Reload -> {
|
||||
// Stop all tasks
|
||||
loadMoreActionJob?.cancel()
|
||||
reloadActionJob?.cancel()
|
||||
stopAllUpdates()
|
||||
reloadActionJob = scope.launch(fetchDispatcher) {
|
||||
reloadTask(action)
|
||||
}
|
||||
}
|
||||
is BatchAction.LoadMore -> {
|
||||
if (loadMoreActionJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
|
||||
loadMoreActionJob = scope.launch(fetchDispatcher) {
|
||||
reloadActionJob?.join()
|
||||
loadMoreTask(action)
|
||||
}
|
||||
}
|
||||
is BatchAction.UpdateBatches -> {
|
||||
if (updateFetcher == null) return
|
||||
|
||||
scope.launch(fetchDispatcher) {
|
||||
// Lazily start a job so we can avoid batch update collisions
|
||||
// by waiting for other tasks with the same keys to complete
|
||||
val job = launch(start = CoroutineStart.LAZY) {
|
||||
updateBatchesTask(action)
|
||||
}
|
||||
|
||||
val actionJob = action to job
|
||||
|
||||
waitingUpdateJobs.update { it + actionJob }
|
||||
|
||||
// Wait for other update tasks that mutate batches with the same keys
|
||||
updateJobs.first { workingJobs ->
|
||||
action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty()
|
||||
}
|
||||
|
||||
waitingUpdateJobs.update { it - actionJob }
|
||||
|
||||
// No other task are mutating batches with the same keys, so we can start a job
|
||||
val started = job.start()
|
||||
|
||||
if (started) {
|
||||
updateJobs.update { it + actionJob }
|
||||
|
||||
job.invokeOnCompletion { cause ->
|
||||
// If the job was cancelled it is up to a canceller to remove job from the updateJobs list
|
||||
if (cause !is CancellationException) {
|
||||
updateJobs.update { it - actionJob }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BatchAction.CancelAllUpdates -> {
|
||||
if (updateFetcher == null) return
|
||||
stopAllUpdates()
|
||||
}
|
||||
is BatchAction.CancelUpdates -> {
|
||||
if (updateFetcher == null) return
|
||||
stopUpdates(action.predicate)
|
||||
}
|
||||
BatchAction.CancelBatchLoading -> {
|
||||
loadMoreActionJob?.cancel()
|
||||
reloadActionJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reloadTask(action: BatchAction.Reload<TRequestParams>) {
|
||||
state.value = BatchListState(
|
||||
data = emptyList(),
|
||||
status = PaginationStatus.InitialLoading,
|
||||
)
|
||||
|
||||
val res = runCatching {
|
||||
batchFetcher.fetchFirst(action.requestParams)
|
||||
}.getOrElse { BatchFetchResult.Error(it) }
|
||||
|
||||
state.value = when (res) {
|
||||
is BatchFetchResult.Success -> {
|
||||
val key = generateNewKey(listOf())
|
||||
val batch = Batch(
|
||||
key = key,
|
||||
data = res.data,
|
||||
)
|
||||
BatchListState(
|
||||
data = listOf(batch),
|
||||
status = if (res.last) {
|
||||
PaginationStatus.EndOfPagination
|
||||
} else {
|
||||
PaginationStatus.Paginating(res)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BatchFetchResult.Error -> {
|
||||
BatchListState(
|
||||
data = emptyList(),
|
||||
status = PaginationStatus.InitialLoadingError(
|
||||
throwable = res.throwable,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
lastRequestResult.value = res
|
||||
}
|
||||
|
||||
private suspend fun loadMoreTask(action: BatchAction.LoadMore<TRequestParams>) {
|
||||
val status = state.value.status
|
||||
|
||||
// Skip the action if the state is not ready to continue pagination.
|
||||
// Two options are acceptable:
|
||||
// 1. The Source is ready to load next page with the same or different request params.
|
||||
// 2. The Source has reached the end of pagination, but there is another request
|
||||
// that can possibly load the next page and continue the pagination
|
||||
|
||||
if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return
|
||||
if (status is PaginationStatus.EndOfPagination && action.requestParams == null) return
|
||||
|
||||
val lastResult = lastRequestResult.value ?: return
|
||||
|
||||
state.update { it.copy(status = PaginationStatus.NextBatchLoading) }
|
||||
|
||||
val res = runCatching {
|
||||
batchFetcher.fetchNext(action.requestParams, lastResult)
|
||||
}.getOrElse { BatchFetchResult.Error(it) }
|
||||
|
||||
lastRequestResult.value = lastResult
|
||||
|
||||
state.update { currentState ->
|
||||
when (res) {
|
||||
is BatchFetchResult.Success -> {
|
||||
val newBatch = Batch(
|
||||
key = generateNewKey(currentState.data.map { it.key }),
|
||||
data = res.data,
|
||||
)
|
||||
|
||||
currentState.copy(
|
||||
data = currentState.data + newBatch,
|
||||
status = if (res.last) {
|
||||
PaginationStatus.EndOfPagination
|
||||
} else {
|
||||
PaginationStatus.Paginating(res)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BatchFetchResult.Error -> {
|
||||
currentState.copy(
|
||||
status = PaginationStatus.Paginating(res),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateBatchesTask(action: BatchAction.UpdateBatches<TKey, TUpdate>) {
|
||||
if (updateFetcher == null) return
|
||||
|
||||
val batches = state.value.data
|
||||
val batchesToUpdate = batches.filter { action.keys.contains(it.key) }
|
||||
|
||||
val result = updateFetcher.fetchUpdate(
|
||||
toUpdate = batchesToUpdate,
|
||||
updateRequest = action.updateRequest,
|
||||
)
|
||||
|
||||
if (result is BatchUpdateResult.Success) {
|
||||
state.update { currentState ->
|
||||
val resMap = result.data.associateBy { it.key }
|
||||
currentState.copy(
|
||||
data = currentState.data.map {
|
||||
resMap[it.key] ?: it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updateResults.emit(action.updateRequest to result)
|
||||
}
|
||||
|
||||
private fun stopAllUpdates() {
|
||||
updateJobs.update { actionJobs ->
|
||||
waitingUpdateJobs.update { waitingActionJobs ->
|
||||
waitingActionJobs.forEach {
|
||||
it.second.cancel()
|
||||
}
|
||||
emptyList()
|
||||
}
|
||||
actionJobs.forEach {
|
||||
it.second.cancel()
|
||||
}
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopUpdates(predicate: (BatchAction.UpdateBatches<TKey, TUpdate>) -> Boolean) {
|
||||
updateJobs.update { actionJobs ->
|
||||
waitingUpdateJobs.update { waitingActionJobs ->
|
||||
waitingActionJobs.mapNotNull {
|
||||
if (predicate(it.first)) {
|
||||
it.second.cancel()
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
actionJobs.mapNotNull {
|
||||
if (predicate(it.first)) {
|
||||
it.second.cancel()
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
/**
|
||||
* State that is used for listening the current state of a pagination.
|
||||
*
|
||||
* @param TKey type of the key of the batch.
|
||||
* @param TData type of the data.
|
||||
*
|
||||
* @property data list of loaded batches.
|
||||
* @property status current status of the pagination.
|
||||
*
|
||||
* @see BatchListSource
|
||||
*/
|
||||
data class BatchListState<TKey, TData>(
|
||||
val data: List<Batch<TKey, TData>>,
|
||||
val status: PaginationStatus<TData>,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
/**
|
||||
* Interface for fetching updates for a batch of data.
|
||||
* Used in [BatchListState].
|
||||
*
|
||||
* @param TKey type of the key.
|
||||
* @param TData type of the data.
|
||||
* @param TUpdate type of the update request.
|
||||
*/
|
||||
fun interface BatchUpdateFetcher<TKey, TData, TUpdate> {
|
||||
|
||||
/**
|
||||
* Fetches updates for a batch of data.
|
||||
* Note that the result batch key as a result of executing the method must be presented in the [toUpdate] list,
|
||||
* otherwise, updates will not be performed
|
||||
*
|
||||
* @param toUpdate list of batches to update.
|
||||
* @param updateRequest request to update the data.
|
||||
* @return result of the update operation.
|
||||
*/
|
||||
suspend fun fetchUpdate(toUpdate: List<Batch<TKey, TData>>, updateRequest: TUpdate): BatchUpdateResult<TKey, TData>
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
/**
|
||||
* Represents the result of a batch fetch operation.
|
||||
* Used in [BatchListState] and [BatchUpdateFetcher].
|
||||
*
|
||||
* @param TKey type of the key.
|
||||
* @param TData type of the data.
|
||||
*/
|
||||
sealed class BatchUpdateResult<out TKey, out TData> {
|
||||
|
||||
/**
|
||||
* Represents a successful result of a batch update operation.
|
||||
*
|
||||
* @param data fetched data.
|
||||
*/
|
||||
data class Success<TKey, TData>(
|
||||
val data: List<Batch<TKey, TData>>,
|
||||
) : BatchUpdateResult<TKey, TData>()
|
||||
|
||||
/**
|
||||
* Represents an error result of a batch update operation.
|
||||
*
|
||||
* @param error error that occurred during the operation.
|
||||
*/
|
||||
class Error(val throwable: Throwable) : BatchUpdateResult<Nothing, Nothing>()
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Context for working with [BatchListSource].
|
||||
*
|
||||
* @param TRequestParams type of the request.
|
||||
* @param TKey type of the key.
|
||||
* @param TUpdate type of the update request.
|
||||
*
|
||||
* @property actionsFlow flow of [BatchAction]s that would be dispatched to [BatchListSource].
|
||||
* @property coroutineScope scope for the [BatchListSource] to launch coroutines. When it is cancelled,
|
||||
* all the operations and requests launched in the [BatchListSource] would be cancelled and all data would be cleared.
|
||||
*
|
||||
* @see BatchListSource
|
||||
*/
|
||||
class BatchingContext<TKey, TRequestParams, TUpdate>(
|
||||
val actionsFlow: Flow<BatchAction<TKey, TRequestParams, TUpdate>>,
|
||||
val coroutineScope: CoroutineScope,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.pagination
|
||||
|
||||
/**
|
||||
* Status of the pagination.
|
||||
*
|
||||
* @param TData type of the data.
|
||||
*
|
||||
* @see BatchListState
|
||||
*/
|
||||
sealed class PaginationStatus<out TData> {
|
||||
|
||||
/**
|
||||
* Represents that there is no data. Used when the list of batches is empty.
|
||||
* The initial state of the pagination.
|
||||
*/
|
||||
data object None : PaginationStatus<Nothing>()
|
||||
|
||||
/**
|
||||
* Represents that the batch is loading for the first time.
|
||||
* Used when the pagination is empty and the first batch is being loaded.
|
||||
*/
|
||||
data object InitialLoading : PaginationStatus<Nothing>()
|
||||
|
||||
/**
|
||||
* Represents that the first batch was loaded with an error.
|
||||
*
|
||||
* @param error error that occurred during the initial loading.
|
||||
*/
|
||||
data class InitialLoadingError(
|
||||
val throwable: Throwable,
|
||||
) : PaginationStatus<Nothing>()
|
||||
|
||||
/**
|
||||
* Represents that the last batch was loaded and
|
||||
* the source is ready to load the next one or reload previous if [lastResult] is an error.
|
||||
* For the first batch, [lastResult] is always [BatchFetchResult.Success]
|
||||
*
|
||||
* @param lastResult result of the last batch fetch.
|
||||
*/
|
||||
data class Paginating<out TData>(
|
||||
val lastResult: BatchFetchResult<TData>,
|
||||
) : PaginationStatus<TData>()
|
||||
|
||||
/**
|
||||
* Represents that the next batch is loading.
|
||||
* Used when the next batch is being loaded.
|
||||
*/
|
||||
data object NextBatchLoading : PaginationStatus<Nothing>()
|
||||
|
||||
/**
|
||||
* Represents that the source has no more batches to load.
|
||||
* The next [BatchAction.LoadMore] with [BatchAction.LoadMore.requestParams] = null will be ignored.
|
||||
*/
|
||||
data object EndOfPagination : PaginationStatus<Nothing>()
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.pagination.exception
|
||||
|
||||
/**
|
||||
* Exception that is thrown when there are no more items to fetch.
|
||||
*/
|
||||
class EndOfPaginationException : IllegalStateException()
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.pagination.fetcher
|
||||
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListState
|
||||
|
||||
/**
|
||||
* Interface for fetching a batch of data. Used in [BatchListState].
|
||||
*
|
||||
* @param TRequestParams type of the request.
|
||||
* @param TData type of the data.
|
||||
*
|
||||
* @see BatchListState
|
||||
*/
|
||||
interface BatchFetcher<TRequestParams : Any, TData> {
|
||||
|
||||
/**
|
||||
* Fetches the first batch of data.
|
||||
*
|
||||
* @param requestParams initial request params. Will be saved to be used in [fetchNext] requests.
|
||||
* @return result of the fetch operation.
|
||||
*/
|
||||
suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<TData>
|
||||
|
||||
/**
|
||||
* Fetches the next batch of data.
|
||||
*
|
||||
* @param overrideRequestParams overrides current remembered request, even if that fetch fails.
|
||||
* If null, the last request should be used.
|
||||
* @param lastResult result of the last fetch operation.
|
||||
* @return result of the fetch operation.
|
||||
*/
|
||||
suspend fun fetchNext(
|
||||
overrideRequestParams: TRequestParams?,
|
||||
lastResult: BatchFetchResult<TData>,
|
||||
): BatchFetchResult<TData>
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.pagination.fetcher
|
||||
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.exception.EndOfPaginationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* Fetcher that uses limit and offset to fetch data.
|
||||
*
|
||||
* @param TRequestParams type of the request params.
|
||||
* @param TData type of the data.
|
||||
*
|
||||
* @property prefetchDistance number of items to fetch for the first batch.
|
||||
* @property batchSize size of the batch.
|
||||
* @property fetch function that fetches the data.
|
||||
*/
|
||||
class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
|
||||
private val prefetchDistance: Int,
|
||||
private val batchSize: Int,
|
||||
private val fetch: suspend (request: Request<TRequestParams>) -> BatchFetchResult<TData>,
|
||||
) : BatchFetcher<TRequestParams, TData> {
|
||||
|
||||
data class Request<TRequest>(
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
val request: TRequest,
|
||||
)
|
||||
|
||||
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
|
||||
|
||||
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<TData> {
|
||||
val req = Request(
|
||||
offset = 0,
|
||||
limit = prefetchDistance,
|
||||
request = requestParams,
|
||||
)
|
||||
|
||||
val res = fetch(req)
|
||||
lastRequest.value = req
|
||||
return res
|
||||
}
|
||||
|
||||
override suspend fun fetchNext(
|
||||
overrideRequestParams: TRequestParams?,
|
||||
lastResult: BatchFetchResult<TData>,
|
||||
): BatchFetchResult<TData> {
|
||||
val last = lastRequest.value
|
||||
requireNotNull(last)
|
||||
|
||||
val req = if (lastResult is BatchFetchResult.Success) {
|
||||
if (lastResult.last && overrideRequestParams == null) {
|
||||
return BatchFetchResult.Error(EndOfPaginationException())
|
||||
}
|
||||
|
||||
Request(
|
||||
offset = last.offset + last.limit,
|
||||
limit = batchSize,
|
||||
request = overrideRequestParams ?: last.request,
|
||||
)
|
||||
} else {
|
||||
last
|
||||
}
|
||||
|
||||
val res = fetch(req)
|
||||
lastRequest.value = req
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +127,7 @@ include(":core:utils")
|
|||
include(":core:deep-links")
|
||||
include(":core:deep-links:global")
|
||||
include(":core:decompose")
|
||||
include(":core:pagination")
|
||||
// endregion Core modules
|
||||
|
||||
// region Common modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue