From 7dd5e87cfd3c7f0615792d8e407aae8babfa8295 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 17:10:03 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../java/com/tangem/pagination/BatchAction.kt | 10 +-- .../com/tangem/pagination/BatchListSource.kt | 64 +++++++++++-------- .../com/tangem/pagination/BatchingContext.kt | 4 +- .../tangem/pagination/fetcher/BatchFetcher.kt | 15 +++-- .../fetcher/LimitOffsetBatchFetcher.kt | 12 ++-- 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 20d95b63aa..691941752a 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -7,7 +7,7 @@ package com.tangem.pagination * @param TKey type of the key of the batch. * @param TUpdate type of the update request. */ -sealed class BatchAction { +sealed class BatchAction { /** * Action to load the first batch. @@ -16,7 +16,7 @@ sealed class BatchAction { */ data class Reload( val requestParams: TRequestParams, - ) : BatchAction() + ) : BatchAction() /** * Action to load the next batch. @@ -27,7 +27,7 @@ sealed class BatchAction { */ data class LoadMore( val requestParams: TRequestParams? = null, - ) : BatchAction() + ) : BatchAction() /** * Action to update the batch. @@ -38,7 +38,7 @@ sealed class BatchAction { class UpdateBatches( val keys: Set, val updateRequest: TUpdate, - ) : BatchAction() + ) : BatchAction() /** * Action to cancel the current batch loading. @@ -57,5 +57,5 @@ sealed class BatchAction { */ class CancelUpdates( val predicate: (UpdateBatches) -> Boolean, - ) : BatchAction() + ) : BatchAction() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index bd521c6f71..fc1d0bd1c2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -30,11 +30,11 @@ interface BatchListSource { * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( +fun BatchListSource( fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, - context: BatchingContext, + context: BatchingContext, generateNewKey: suspend (List) -> TKey, - batchFetcher: BatchFetcher, + batchFetcher: BatchFetcher, ): BatchListSource = DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null) @@ -50,18 +50,18 @@ fun BatchListSource( * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( +fun BatchListSource( fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, - context: BatchingContext, + context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, updateFetcher: BatchUpdateFetcher, ): BatchListSource = DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) -private class DefaultBatchListSource( +private class DefaultBatchListSource( private val fetchDispatcher: CoroutineDispatcher, - private val context: BatchingContext, + private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, private val batchFetcher: BatchFetcher, private val updateFetcher: BatchUpdateFetcher? = null, @@ -104,7 +104,7 @@ private class DefaultBatchListSource } } - private fun collectActions(action: BatchAction) { + private fun collectActions(action: BatchAction) { when (action) { is BatchAction.Reload -> { // Stop all tasks @@ -129,6 +129,8 @@ private class DefaultBatchListSource 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) } @@ -137,18 +139,21 @@ private class DefaultBatchListSource 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 } } @@ -213,6 +218,12 @@ private class DefaultBatchListSource private suspend fun loadMoreTask(action: BatchAction.LoadMore) { 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 @@ -227,24 +238,27 @@ private class DefaultBatchListSource lastRequestResult.value = lastResult state.update { currentState -> - if (res is BatchFetchResult.Success) { - val newBatch = Batch( - key = generateNewKey(currentState.data.map { it.key }), - data = res.data, - ) + 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) - }, - ) - } else { - currentState.copy( - status = PaginationStatus.Paginating(res), - ) + 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), + ) + } } } } diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt index fb9672d365..252d6f1680 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.Flow * * @see BatchListSource */ -class BatchingContext( - val actionsFlow: Flow>, +class BatchingContext( + val actionsFlow: Flow>, val coroutineScope: CoroutineScope, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt index 98358257b5..818620dab1 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt @@ -6,28 +6,31 @@ import com.tangem.pagination.BatchListState /** * Interface for fetching a batch of data. Used in [BatchListState]. * - * @param TRequest type of the request. + * @param TRequestParams type of the request. * @param TData type of the data. * * @see BatchListState */ -interface BatchFetcher { +interface BatchFetcher { /** * Fetches the first batch of data. * - * @param request initial request. Will be saved to be used in [fetchNext] requests. + * @param requestParams initial request params. Will be saved to be used in [fetchNext] requests. * @return result of the fetch operation. */ - suspend fun fetchFirst(request: TRequest): BatchFetchResult + suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult /** * Fetches the next batch of data. * - * @param overrideRequest overrides current remembered request, even if that fetch fails. + * @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(overrideRequest: TRequest?, lastResult: BatchFetchResult): BatchFetchResult + suspend fun fetchNext( + overrideRequestParams: TRequestParams?, + lastResult: BatchFetchResult, + ): BatchFetchResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 6a321a61e9..01059c4676 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class LimitOffsetBatchFetcher( private val prefetchDistance: Int, private val batchSize: Int, - private val fetch: (request: Request) -> BatchFetchResult, + private val fetch: suspend (request: Request) -> BatchFetchResult, ) : BatchFetcher { data class Request( @@ -28,11 +28,11 @@ class LimitOffsetBatchFetcher( private val lastRequest = MutableStateFlow?>(null) - override suspend fun fetchFirst(request: TRequestParams): BatchFetchResult { + override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult { val req = Request( offset = 0, limit = prefetchDistance, - request = request, + request = requestParams, ) val res = fetch(req) @@ -41,21 +41,21 @@ class LimitOffsetBatchFetcher( } override suspend fun fetchNext( - overrideRequest: TRequestParams?, + overrideRequestParams: TRequestParams?, lastResult: BatchFetchResult, ): BatchFetchResult { val last = lastRequest.value requireNotNull(last) val req = if (lastResult is BatchFetchResult.Success) { - if (lastResult.last && overrideRequest == null) { + if (lastResult.last && overrideRequestParams == null) { return BatchFetchResult.Error(EndOfPaginationException()) } Request( offset = last.offset + last.limit, limit = batchSize, - request = overrideRequest ?: last.request, + request = overrideRequestParams ?: last.request, ) } else { last