From 8737ebd2911323e8b5d8ea89cf006ecc911fbab9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 14:09:16 +0300 Subject: [PATCH 1/7] Updated on 2026-08-14 --- core/pagination/.gitignore | 1 + core/pagination/build.gradle.kts | 10 + .../main/java/com/tangem/pagination/Batch.kt | 13 + .../java/com/tangem/pagination/BatchAction.kt | 61 ++++ .../com/tangem/pagination/BatchFetchResult.kt | 37 ++ .../pagination/BatchFetchUpdateResult.kt | 36 ++ .../com/tangem/pagination/BatchFetcher.kt | 21 ++ .../com/tangem/pagination/BatchListSource.kt | 343 ++++++++++++++++++ .../com/tangem/pagination/BatchListState.kt | 18 + .../com/tangem/pagination/BatchRequest.kt | 18 + .../tangem/pagination/BatchUpdateFetcher.kt | 25 ++ .../com/tangem/pagination/BatchingConfig.kt | 12 + .../com/tangem/pagination/BatchingContext.kt | 22 ++ .../com/tangem/pagination/PaginationStatus.kt | 56 +++ settings.gradle.kts | 1 + 15 files changed, 674 insertions(+) create mode 100644 core/pagination/.gitignore create mode 100644 core/pagination/build.gradle.kts create mode 100644 core/pagination/src/main/java/com/tangem/pagination/Batch.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt diff --git a/core/pagination/.gitignore b/core/pagination/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/pagination/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/pagination/build.gradle.kts b/core/pagination/build.gradle.kts new file mode 100644 index 0000000000..c7b5d3d97a --- /dev/null +++ b/core/pagination/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + // region Coroutines + implementation(deps.kotlin.coroutines) + // endregion +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/Batch.kt b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt new file mode 100644 index 0000000000..b4b2482eaa --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt @@ -0,0 +1,13 @@ +package com.tangem.pagination + +/** + * Represents a batch of data with a key. + * Used in [BatchListState]. + * + * @param K type of the key. + * @param T type of the data. + */ +data class Batch( + val key: K, + val data: T, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt new file mode 100644 index 0000000000..710e191dd7 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -0,0 +1,61 @@ +package com.tangem.pagination + +/** + * Action that can be dispatched to [BatchListSource]. + * + * @param R type of the request to load batches. + * @param K type of the key of the batch. + * @param U type of the update request. + */ +sealed class BatchAction { + + /** + * Action to load the first batch. + * + * @param request request to load the first batch. + */ + data class Reload( + val request: R, + ) : BatchAction() + + /** + * Action to load the next batch. + * + * @param request request 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( + val request: R? = null, + ) : BatchAction() + + /** + * Action to update the batch. + * + * @param keys keys of the batches to update. + * @param request request to update the batches. + */ + class UpdateBatches( + val keys: Set, + val request: U, + ) : BatchAction() + + /** + * Action to cancel the current batch loading. + */ + data object CancelBatchLoading : BatchAction() + + /** + * Action to cancel all update requests. + */ + data object CancelAllUpdates : BatchAction() + + /** + * Action to cancel update requests that satisfy the predicate. + * + * @param predicate predicate to check if the update request should be cancelled. + */ + class CancelUpdates( + val predicate: (UpdateBatches) -> Boolean, + ) : BatchAction() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt new file mode 100644 index 0000000000..d4ea1d4ae7 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -0,0 +1,37 @@ +package com.tangem.pagination + +/** + * Represents a result of a batch fetch request. + * Used in [BatchListState]. + * + * @param T type of the data. + * @param E type of the error. + */ +sealed class BatchFetchResult { + + /** + * 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( + val data: T, + val last: Boolean = false, + ) : BatchFetchResult() + + /** + * Represents an error result of a batch fetch request. + * + * @param error error that occurred during the request. + */ + data class Error(val error: E) : BatchFetchResult() + + /** + * Represents an unknown error result of a batch fetch request. + * Used for unexpected exceptions that occurred in fetch method in [BatchFetcher]. + * + * @param throwable throwable that occurred during the request. + */ + class UnknownError(val throwable: Throwable) : BatchFetchResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt new file mode 100644 index 0000000000..7d47ed8142 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt @@ -0,0 +1,36 @@ +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. + * @param TError type of the error. + */ +sealed class BatchFetchUpdateResult { + + /** + * Represents a successful result of a batch update operation. + * + * @param data fetched data. + */ + data class Success( + val data: List>, + ) : BatchFetchUpdateResult() + + /** + * Represents an error result of a batch update operation. + * + * @param error error that occurred during the operation. + */ + data class Error(val error: TError) : BatchFetchUpdateResult() + + /** + * Represents an unknown error result of a batch update operation. + * Used for unexpected exceptions that occurred in `fetchUpdate` method in [BatchUpdateFetcher]. + * + * @param throwable throwable that occurred during the operation. + */ + class UnknownError(val throwable: Throwable) : BatchFetchUpdateResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt new file mode 100644 index 0000000000..aea9c1bd37 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt @@ -0,0 +1,21 @@ +package com.tangem.pagination + +/** + * Interface for fetching a batch of data. Used in [BatchListState]. + * + * @param TRequest type of the request. + * @param TData type of the data. + * @param TError type of the error. + * + * @see BatchListState + */ +interface BatchFetcher { + + /** + * Fetches a batch of data. + * + * @param request request to fetch the data. + * @return result of the fetch operation. + */ + suspend fun fetch(request: BatchRequest): BatchFetchResult +} \ 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 new file mode 100644 index 0000000000..a3e529d029 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -0,0 +1,343 @@ +package com.tangem.pagination + +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. + * @param TError Type of the error that can occur during fetching or updating. + * @property state State of the paginated data. + * @property updateResults Flow of results of update requests. + */ +interface BatchListSource { + val state: StateFlow> + val updateResults: SharedFlow>> +} + +/** + * Creates a new [BatchListSource] with the provided configuration. + * + * @param config Configuration for batching. + * @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 BatchListSource( + config: BatchingConfig, + context: BatchingContext, + generateNewKey: suspend (List) -> TKey, + batchFetcher: BatchFetcher, +): BatchListSource = + BatchListSourceImpl(config, context, generateNewKey, batchFetcher, null) + +/** + * Creates a new [BatchListSource] with the provided configuration. + * + * @param config Configuration for batching. + * @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 BatchListSource( + config: BatchingConfig, + context: BatchingContext, + generateNewKey: suspend (List) -> TKey, + batchFetcher: BatchFetcher, + updateFetcher: BatchUpdateFetcher, +): BatchListSource = + BatchListSourceImpl(config, context, generateNewKey, batchFetcher, updateFetcher) + +private class BatchListSourceImpl( + private val config: BatchingConfig, + private val context: BatchingContext, + private val generateNewKey: suspend (List) -> TKey, + private val batchFetcher: BatchFetcher, + private val updateFetcher: BatchUpdateFetcher? = null, +) : BatchListSource { + + override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) + override val updateResults = MutableSharedFlow>>( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + private val scope = context.coroutineScope + private val updateJobs = MutableStateFlow, Job>>>(emptyList()) + private val waitingUpdateJobs = + MutableStateFlow, Job>>>(emptyList()) + + private val lastRequest = MutableStateFlow?>(null) + private var reloadActionJob: Job? = null + private var loadMoreActionJob: Job? = null + + init { + scope.launch { + try { + awaitCancellation() + } finally { + withContext(NonCancellable) { + lastRequest.value = null + loadMoreActionJob = null + loadMoreActionJob = null + lastRequest.value = null + stopAllUpdates() + state.value = BatchListState(emptyList(), PaginationStatus.None) + } + } + } + + scope.launch { + context.actionsFlow.collect { action -> + collectActions(action) + } + } + } + + private fun collectActions(action: BatchAction) { + when (action) { + is BatchAction.Reload -> { + // Stop all tasks + loadMoreActionJob?.cancel() + reloadActionJob?.cancel() + stopAllUpdates() + reloadActionJob = scope.launch(Dispatchers.IO) { + reloadTask(action) + } + } + is BatchAction.LoadMore -> { + if (loadMoreActionJob?.isActive == true) { + return + } + + loadMoreActionJob = scope.launch(Dispatchers.IO) { + reloadActionJob?.join() + loadMoreTask(action) + } + } + is BatchAction.UpdateBatches -> { + if (updateFetcher == null) return + + scope.launch(Dispatchers.IO) { + val job = launch(start = CoroutineStart.LAZY) { + updateBatchesTask(action) + } + + val actionJob = action to job + + waitingUpdateJobs.update { it + actionJob } + + updateJobs.first { workingJobs -> + action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() + } + + waitingUpdateJobs.update { it - actionJob } + + val started = job.start() + + if (started) { + updateJobs.update { it + actionJob } + + job.invokeOnCompletion { cause -> + 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) { + state.value = BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoading, + ) + + val requestData = action.request + val request = BatchRequest(offset = 0, limit = config.batchSize, requestData) + + val res = runCatching { + batchFetcher.fetch(request) + }.getOrElse { BatchFetchResult.UnknownError(it) } + + state.value = if (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) + }, + ) + } else { + BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoadingError( + error = (res as? BatchFetchResult.Error)?.error, + ), + ) + } + + lastRequest.value = request + } + + private suspend fun loadMoreTask(action: BatchAction.LoadMore) { + val status = state.value.status + val lastReq = lastRequest.value + + val request: BatchRequest = when { + // try to continue pagination with new request + status is PaginationStatus.EndOfPagination && action.request != null && action.request != lastReq -> { + requireNotNull(lastReq) + + BatchRequest( + offset = lastReq.offset + lastReq.limit, + limit = config.batchSize, + data = action.request, + ) + } + // continue pagination + status is PaginationStatus.Paginating -> { + requireNotNull(lastReq) + + if (status.lastResult is BatchFetchResult.Success) { + BatchRequest( + offset = lastReq.offset + lastReq.limit, + limit = config.batchSize, + data = action.request ?: lastReq.data, + ) + } else { + BatchRequest( + offset = lastReq.offset, + limit = lastReq.limit, + data = action.request ?: lastReq.data, + ) + } + } + else -> return + } + + state.update { it.copy(status = PaginationStatus.NextBatchLoading) } + + val res = runCatching { + batchFetcher.fetch(request) + }.getOrElse { BatchFetchResult.UnknownError(it) } + + lastRequest.value = request + + state.update { currentState -> + if (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), + ) + } + } + } + + private suspend fun updateBatchesTask(action: BatchAction.UpdateBatches) { + 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.request, + ) + + if (result is BatchFetchUpdateResult.Success) { + state.update { currentState -> + val resMap = result.data.associateBy { it.key } + currentState.copy( + data = currentState.data.map { + resMap[it.key] ?: it + }, + ) + } + } + + updateResults.emit(action.request 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) -> 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 + } + } + } + } +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt new file mode 100644 index 0000000000..adce142dab --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -0,0 +1,18 @@ +package com.tangem.pagination + +/** + * State that is used for listening the current state of a pagination. + * + * @param K type of the key of the batch. + * @param T type of the data. + * @param E type of the error. + * + * @property data list of loaded batches. + * @property status current status of the pagination. + * + * @see BatchListSource + */ +data class BatchListState( + val data: List>, + val status: PaginationStatus, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt new file mode 100644 index 0000000000..6710c62702 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt @@ -0,0 +1,18 @@ +package com.tangem.pagination + +/** + * Request to fetch a batch of data. + * + * @param R type of the request. + * + * @property offset offset for the request. + * @property limit limit for the request. + * @property data body of the request. + * + * @see BatchFetcher + */ +data class BatchRequest( + val offset: Int, + val limit: Int, + val data: R, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt new file mode 100644 index 0000000000..71ddd43c76 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -0,0 +1,25 @@ +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 TError type of the error. + * @param TUpdate type of the update request. + */ +interface BatchUpdateFetcher { + + /** + * Fetches updates for a batch of data. + * + * @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>, + updateRequest: TUpdate, + ): BatchFetchUpdateResult +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt new file mode 100644 index 0000000000..8789d11405 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt @@ -0,0 +1,12 @@ +package com.tangem.pagination + +/** + * Configuration for batching. + * + * @param batchSize size of the batch. + * @param prefetchDistance number of items to fetch for the first batch. + */ +data class BatchingConfig( + val batchSize: Int, + val prefetchDistance: Int = batchSize, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt new file mode 100644 index 0000000000..95f10045c4 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -0,0 +1,22 @@ +package com.tangem.pagination + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow + +/** + * Context for working with [BatchListSource]. + * + * @param R type of the request. + * @param K type of the key. + * @param U 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( + val actionsFlow: Flow>, + val coroutineScope: CoroutineScope, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt new file mode 100644 index 0000000000..054178ce19 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt @@ -0,0 +1,56 @@ +package com.tangem.pagination + +/** + * Status of the pagination. + * + * @param T type of the data. + * @param E type of the error. + * + * @see BatchListState + */ +sealed class PaginationStatus { + + /** + * Represents that there is no data. Used when the list of batches is empty. + * The initial state of the pagination. + */ + data object None : PaginationStatus() + + /** + * 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() + + /** + * Represents that the first batch was loaded with an error. + * + * @param error error that occurred during the initial loading. + */ + data class InitialLoadingError( + val error: E?, + ) : PaginationStatus() + + /** + * 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( + val lastResult: BatchFetchResult, + ) : PaginationStatus() + + /** + * Represents that the next batch is loading. + * Used when the next batch is being loaded. + */ + data object NextBatchLoading : PaginationStatus() + + /** + * Represents that the source has no more batches to load. + * The next [BatchAction.LoadMore] with [BatchAction.LoadMore.request] = null will be ignored. + */ + data object EndOfPagination : PaginationStatus() +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3637dd3cc9..6d1f411939 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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 From 5c51d8c3f16d3c500dbacc1b98564f876d44416e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 14:33:31 +0300 Subject: [PATCH 2/7] Updated on 2026-08-14 --- .../src/main/java/com/tangem/pagination/Batch.kt | 10 +++++----- .../main/java/com/tangem/pagination/BatchAction.kt | 8 ++++---- .../java/com/tangem/pagination/BatchFetchResult.kt | 14 +++++++------- .../java/com/tangem/pagination/BatchListState.kt | 12 ++++++------ .../java/com/tangem/pagination/BatchRequest.kt | 6 +++--- .../java/com/tangem/pagination/BatchingContext.kt | 10 +++++----- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/core/pagination/src/main/java/com/tangem/pagination/Batch.kt b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt index b4b2482eaa..0e262776a2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/Batch.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt @@ -4,10 +4,10 @@ package com.tangem.pagination * Represents a batch of data with a key. * Used in [BatchListState]. * - * @param K type of the key. - * @param T type of the data. + * @param TKey type of the key. + * @param TData type of the data. */ -data class Batch( - val key: K, - val data: T, +data class Batch( + val key: TKey, + val data: TData, ) \ No newline at end of file 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 710e191dd7..4a4cfd7d71 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -3,11 +3,11 @@ package com.tangem.pagination /** * Action that can be dispatched to [BatchListSource]. * - * @param R type of the request to load batches. - * @param K type of the key of the batch. - * @param U type of the update request. + * @param TRequest type of the request to load batches. + * @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. diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index d4ea1d4ae7..03f87be764 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -4,10 +4,10 @@ package com.tangem.pagination * Represents a result of a batch fetch request. * Used in [BatchListState]. * - * @param T type of the data. - * @param E type of the error. + * @param TData type of the data. + * @param TError type of the error. */ -sealed class BatchFetchResult { +sealed class BatchFetchResult { /** * Represents a successful result of a batch fetch request. @@ -15,17 +15,17 @@ sealed class BatchFetchResult { * @param data fetched data. * @param last indicates if this is the last batch for the request. */ - data class Success( - val data: T, + data class Success( + val data: TData, val last: Boolean = false, - ) : BatchFetchResult() + ) : BatchFetchResult() /** * Represents an error result of a batch fetch request. * * @param error error that occurred during the request. */ - data class Error(val error: E) : BatchFetchResult() + data class Error(val error: TError) : BatchFetchResult() /** * Represents an unknown error result of a batch fetch request. diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt index adce142dab..525458362f 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -3,16 +3,16 @@ package com.tangem.pagination /** * State that is used for listening the current state of a pagination. * - * @param K type of the key of the batch. - * @param T type of the data. - * @param E type of the error. + * @param TKey type of the key of the batch. + * @param TData type of the data. + * @param TError type of the error. * * @property data list of loaded batches. * @property status current status of the pagination. * * @see BatchListSource */ -data class BatchListState( - val data: List>, - val status: PaginationStatus, +data class BatchListState( + val data: List>, + val status: PaginationStatus, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt index 6710c62702..b6c9545527 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt @@ -3,7 +3,7 @@ package com.tangem.pagination /** * Request to fetch a batch of data. * - * @param R type of the request. + * @param TRequest type of the request. * * @property offset offset for the request. * @property limit limit for the request. @@ -11,8 +11,8 @@ package com.tangem.pagination * * @see BatchFetcher */ -data class BatchRequest( +data class BatchRequest( val offset: Int, val limit: Int, - val data: R, + val data: TRequest, ) \ No newline at end of file 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 95f10045c4..37b6bd2c73 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.Flow /** * Context for working with [BatchListSource]. * - * @param R type of the request. - * @param K type of the key. - * @param U type of the update request. + * @param TRequest 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, @@ -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 From dc97dbd91fa4c77020843d38fc77f846d259a816 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 19:09:34 +0300 Subject: [PATCH 3/7] Updated on 2026-08-14 --- .../java/com/tangem/pagination/BatchAction.kt | 26 +++---- .../com/tangem/pagination/BatchFetchResult.kt | 3 +- .../com/tangem/pagination/BatchFetcher.kt | 21 ------ .../com/tangem/pagination/BatchListSource.kt | 65 ++++------------- .../com/tangem/pagination/BatchRequest.kt | 18 ----- .../exception/EndOfPaginationException.kt | 6 ++ .../tangem/pagination/fetcher/BatchFetcher.kt | 37 ++++++++++ .../fetcher/LimitOffsetBatchFetcher.kt | 69 +++++++++++++++++++ 8 files changed, 142 insertions(+), 103 deletions(-) delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt 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 4a4cfd7d71..813cbe6ba9 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -14,9 +14,9 @@ sealed class BatchAction { * * @param request request to load the first batch. */ - data class Reload( - val request: R, - ) : BatchAction() + data class Reload( + val request: TRequest, + ) : BatchAction() /** * Action to load the next batch. @@ -25,9 +25,9 @@ sealed class BatchAction { * 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( - val request: R? = null, - ) : BatchAction() + data class LoadMore( + val request: TRequest? = null, + ) : BatchAction() /** * Action to update the batch. @@ -35,10 +35,10 @@ sealed class BatchAction { * @param keys keys of the batches to update. * @param request request to update the batches. */ - class UpdateBatches( - val keys: Set, - val request: U, - ) : BatchAction() + class UpdateBatches( + val keys: Set, + val request: TUpdate, + ) : BatchAction() /** * Action to cancel the current batch loading. @@ -55,7 +55,7 @@ sealed class BatchAction { * * @param predicate predicate to check if the update request should be cancelled. */ - class CancelUpdates( - val predicate: (UpdateBatches) -> Boolean, - ) : BatchAction() + class CancelUpdates( + val predicate: (UpdateBatches) -> Boolean, + ) : BatchAction() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index 03f87be764..c3b8b03ee2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -29,9 +29,10 @@ sealed class BatchFetchResult { /** * Represents an unknown error result of a batch fetch request. - * Used for unexpected exceptions that occurred in fetch method in [BatchFetcher]. + * 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 UnknownError(val throwable: Throwable) : BatchFetchResult() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt deleted file mode 100644 index aea9c1bd37..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.pagination - -/** - * Interface for fetching a batch of data. Used in [BatchListState]. - * - * @param TRequest type of the request. - * @param TData type of the data. - * @param TError type of the error. - * - * @see BatchListState - */ -interface BatchFetcher { - - /** - * Fetches a batch of data. - * - * @param request request to fetch the data. - * @return result of the fetch operation. - */ - suspend fun fetch(request: BatchRequest): BatchFetchResult -} \ 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 a3e529d029..7021d474c3 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -1,5 +1,6 @@ package com.tangem.pagination +import com.tangem.pagination.fetcher.BatchFetcher import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* @@ -30,13 +31,11 @@ interface BatchListSource { * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - config: BatchingConfig, +fun BatchListSource( context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, -): BatchListSource = - BatchListSourceImpl(config, context, generateNewKey, batchFetcher, null) +): BatchListSource = BatchListSourceImpl(context, generateNewKey, batchFetcher, null) /** * Creates a new [BatchListSource] with the provided configuration. @@ -50,17 +49,15 @@ fun BatchListSource( * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - config: BatchingConfig, +fun BatchListSource( context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, updateFetcher: BatchUpdateFetcher, ): BatchListSource = - BatchListSourceImpl(config, context, generateNewKey, batchFetcher, updateFetcher) + BatchListSourceImpl(context, generateNewKey, batchFetcher, updateFetcher) -private class BatchListSourceImpl( - private val config: BatchingConfig, +private class BatchListSourceImpl( private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, private val batchFetcher: BatchFetcher, @@ -78,7 +75,7 @@ private class BatchListSourceImpl( private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) - private val lastRequest = MutableStateFlow?>(null) + private val lastRequestResult = MutableStateFlow?>(null) private var reloadActionJob: Job? = null private var loadMoreActionJob: Job? = null @@ -88,10 +85,9 @@ private class BatchListSourceImpl( awaitCancellation() } finally { withContext(NonCancellable) { - lastRequest.value = null loadMoreActionJob = null loadMoreActionJob = null - lastRequest.value = null + lastRequestResult.value = null stopAllUpdates() state.value = BatchListState(emptyList(), PaginationStatus.None) } @@ -178,11 +174,8 @@ private class BatchListSourceImpl( status = PaginationStatus.InitialLoading, ) - val requestData = action.request - val request = BatchRequest(offset = 0, limit = config.batchSize, requestData) - val res = runCatching { - batchFetcher.fetch(request) + batchFetcher.fetchFirst(action.request) }.getOrElse { BatchFetchResult.UnknownError(it) } state.value = if (res is BatchFetchResult.Success) { @@ -208,52 +201,24 @@ private class BatchListSourceImpl( ) } - lastRequest.value = request + lastRequestResult.value = res } private suspend fun loadMoreTask(action: BatchAction.LoadMore) { val status = state.value.status - val lastReq = lastRequest.value - val request: BatchRequest = when { - // try to continue pagination with new request - status is PaginationStatus.EndOfPagination && action.request != null && action.request != lastReq -> { - requireNotNull(lastReq) + if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return + if (status is PaginationStatus.EndOfPagination && action.request == null) return - BatchRequest( - offset = lastReq.offset + lastReq.limit, - limit = config.batchSize, - data = action.request, - ) - } - // continue pagination - status is PaginationStatus.Paginating -> { - requireNotNull(lastReq) - - if (status.lastResult is BatchFetchResult.Success) { - BatchRequest( - offset = lastReq.offset + lastReq.limit, - limit = config.batchSize, - data = action.request ?: lastReq.data, - ) - } else { - BatchRequest( - offset = lastReq.offset, - limit = lastReq.limit, - data = action.request ?: lastReq.data, - ) - } - } - else -> return - } + val lastResult = lastRequestResult.value ?: return state.update { it.copy(status = PaginationStatus.NextBatchLoading) } val res = runCatching { - batchFetcher.fetch(request) + batchFetcher.fetchNext(action.request, lastResult) }.getOrElse { BatchFetchResult.UnknownError(it) } - lastRequest.value = request + lastRequestResult.value = lastResult state.update { currentState -> if (res is BatchFetchResult.Success) { diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt deleted file mode 100644 index b6c9545527..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.pagination - -/** - * Request to fetch a batch of data. - * - * @param TRequest type of the request. - * - * @property offset offset for the request. - * @property limit limit for the request. - * @property data body of the request. - * - * @see BatchFetcher - */ -data class BatchRequest( - val offset: Int, - val limit: Int, - val data: TRequest, -) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt b/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt new file mode 100644 index 0000000000..f2cc69767a --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt @@ -0,0 +1,6 @@ +package com.tangem.pagination.exception + +/** + * Exception that is thrown when there are no more items to fetch. + */ +class EndOfPaginationException : IllegalStateException() \ 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 new file mode 100644 index 0000000000..901d83f32b --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt @@ -0,0 +1,37 @@ +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 TRequest type of the request. + * @param TData type of the data. + * @param TError type of the error. + * + * @see BatchListState + */ +interface BatchFetcher { + + /** + * Fetches the first batch of data. + * + * @param request initial request. Will be saved to be used in [fetchNext] requests. + * @return result of the fetch operation. + */ + suspend fun fetchFirst(request: TRequest): BatchFetchResult + + /** + * Fetches the next batch of data. + * + * @param overrideRequest 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 +} \ 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 new file mode 100644 index 0000000000..11af45665d --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -0,0 +1,69 @@ +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 TRequest type of the request. + * @param TData type of the data. + * @param TError type of the error. + * + * @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( + private val prefetchDistance: Int, + private val batchSize: Int, + private val fetch: (request: Request) -> BatchFetchResult, +) : BatchFetcher { + + data class Request( + val limit: Int, + val offset: Int, + val request: TRequest, + ) + + private val lastRequest = MutableStateFlow?>(null) + + override suspend fun fetchFirst(request: TRequest): BatchFetchResult { + val req = Request( + offset = 0, + limit = prefetchDistance, + request = request, + ) + + val res = fetch(req) + lastRequest.value = req + return res + } + + override suspend fun fetchNext( + overrideRequest: TRequest?, + lastResult: BatchFetchResult, + ): BatchFetchResult { + val last = lastRequest.value + requireNotNull(last) + + val req = if (lastResult is BatchFetchResult.Success) { + if (lastResult.last && overrideRequest == null) { + return BatchFetchResult.UnknownError(EndOfPaginationException()) + } + + Request( + offset = last.offset + last.limit, + limit = batchSize, + request = overrideRequest ?: last.request, + ) + } else { + last + } + + val res = fetch(req) + lastRequest.value = req + return res + } +} \ No newline at end of file From dd43f3dbf5063f10f4623345e2df266500491a45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 19:27:25 +0300 Subject: [PATCH 4/7] Updated on 2026-08-14 --- .../com/tangem/pagination/BatchListSource.kt | 36 ++++++++++--------- .../tangem/pagination/BatchUpdateFetcher.kt | 2 +- .../{BatchFetchResult.kt => FetchResult.kt} | 8 ++--- ...chUpdateResult.kt => FetchUpdateResult.kt} | 8 ++--- .../com/tangem/pagination/PaginationStatus.kt | 4 +-- .../tangem/pagination/fetcher/BatchFetcher.kt | 8 ++--- .../fetcher/LimitOffsetBatchFetcher.kt | 14 ++++---- 7 files changed, 42 insertions(+), 38 deletions(-) rename core/pagination/src/main/java/com/tangem/pagination/{BatchFetchResult.kt => FetchResult.kt} (77%) rename core/pagination/src/main/java/com/tangem/pagination/{BatchFetchUpdateResult.kt => FetchUpdateResult.kt} (72%) 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 7021d474c3..cfedfbcbf2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -17,13 +17,13 @@ import kotlinx.coroutines.flow.* */ interface BatchListSource { val state: StateFlow> - val updateResults: SharedFlow>> + val updateResults: SharedFlow>> } /** * Creates a new [BatchListSource] with the provided configuration. * - * @param config Configuration for batching. + * @param ioDispatcher Dispatcher for IO 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. @@ -32,15 +32,17 @@ interface BatchListSource { */ @Suppress("FunctionNaming") fun BatchListSource( + ioDispatcher : CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, -): BatchListSource = BatchListSourceImpl(context, generateNewKey, batchFetcher, null) +): BatchListSource = + BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, null) /** * Creates a new [BatchListSource] with the provided configuration. * - * @param config Configuration for batching. + * @param ioDispatcher Dispatcher for IO 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. @@ -50,14 +52,16 @@ fun BatchListSource( */ @Suppress("FunctionNaming") fun BatchListSource( + ioDispatcher: CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, updateFetcher: BatchUpdateFetcher, ): BatchListSource = - BatchListSourceImpl(context, generateNewKey, batchFetcher, updateFetcher) + BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, updateFetcher) private class BatchListSourceImpl( + private val ioDispatcher: CoroutineDispatcher, private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, private val batchFetcher: BatchFetcher, @@ -65,7 +69,7 @@ private class BatchListSourceImpl( ) : BatchListSource { override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) - override val updateResults = MutableSharedFlow>>( + override val updateResults = MutableSharedFlow>>( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -75,7 +79,7 @@ private class BatchListSourceImpl( private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) - private val lastRequestResult = MutableStateFlow?>(null) + private val lastRequestResult = MutableStateFlow?>(null) private var reloadActionJob: Job? = null private var loadMoreActionJob: Job? = null @@ -108,7 +112,7 @@ private class BatchListSourceImpl( loadMoreActionJob?.cancel() reloadActionJob?.cancel() stopAllUpdates() - reloadActionJob = scope.launch(Dispatchers.IO) { + reloadActionJob = scope.launch(ioDispatcher) { reloadTask(action) } } @@ -117,7 +121,7 @@ private class BatchListSourceImpl( return } - loadMoreActionJob = scope.launch(Dispatchers.IO) { + loadMoreActionJob = scope.launch(ioDispatcher) { reloadActionJob?.join() loadMoreTask(action) } @@ -125,7 +129,7 @@ private class BatchListSourceImpl( is BatchAction.UpdateBatches -> { if (updateFetcher == null) return - scope.launch(Dispatchers.IO) { + scope.launch(ioDispatcher) { val job = launch(start = CoroutineStart.LAZY) { updateBatchesTask(action) } @@ -176,9 +180,9 @@ private class BatchListSourceImpl( val res = runCatching { batchFetcher.fetchFirst(action.request) - }.getOrElse { BatchFetchResult.UnknownError(it) } + }.getOrElse { FetchResult.UnknownError(it) } - state.value = if (res is BatchFetchResult.Success) { + state.value = if (res is FetchResult.Success) { val key = generateNewKey(listOf()) val batch = Batch( key = key, @@ -196,7 +200,7 @@ private class BatchListSourceImpl( BatchListState( data = emptyList(), status = PaginationStatus.InitialLoadingError( - error = (res as? BatchFetchResult.Error)?.error, + error = (res as? FetchResult.Error)?.error, ), ) } @@ -216,12 +220,12 @@ private class BatchListSourceImpl( val res = runCatching { batchFetcher.fetchNext(action.request, lastResult) - }.getOrElse { BatchFetchResult.UnknownError(it) } + }.getOrElse { FetchResult.UnknownError(it) } lastRequestResult.value = lastResult state.update { currentState -> - if (res is BatchFetchResult.Success) { + if (res is FetchResult.Success) { val newBatch = Batch( key = generateNewKey(currentState.data.map { it.key }), data = res.data, @@ -254,7 +258,7 @@ private class BatchListSourceImpl( updateRequest = action.request, ) - if (result is BatchFetchUpdateResult.Success) { + if (result is FetchUpdateResult.Success) { state.update { currentState -> val resMap = result.data.associateBy { it.key } currentState.copy( diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt index 71ddd43c76..1cf7e10a91 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -21,5 +21,5 @@ interface BatchUpdateFetcher { suspend fun fetchUpdate( toUpdate: List>, updateRequest: TUpdate, - ): BatchFetchUpdateResult + ): FetchUpdateResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt similarity index 77% rename from core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt rename to core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt index c3b8b03ee2..d72c42ac1d 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt @@ -7,7 +7,7 @@ package com.tangem.pagination * @param TData type of the data. * @param TError type of the error. */ -sealed class BatchFetchResult { +sealed class FetchResult { /** * Represents a successful result of a batch fetch request. @@ -18,14 +18,14 @@ sealed class BatchFetchResult { data class Success( val data: TData, val last: Boolean = false, - ) : BatchFetchResult() + ) : FetchResult() /** * Represents an error result of a batch fetch request. * * @param error error that occurred during the request. */ - data class Error(val error: TError) : BatchFetchResult() + data class Error(val error: TError) : FetchResult() /** * Represents an unknown error result of a batch fetch request. @@ -34,5 +34,5 @@ sealed class BatchFetchResult { * @param throwable throwable that occurred during the request. * @see com.tangem.pagination.fetcher.BatchFetcher */ - class UnknownError(val throwable: Throwable) : BatchFetchResult() + class UnknownError(val throwable: Throwable) : FetchResult() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt similarity index 72% rename from core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt rename to core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt index 7d47ed8142..d575044a1f 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt @@ -8,7 +8,7 @@ package com.tangem.pagination * @param TData type of the data. * @param TError type of the error. */ -sealed class BatchFetchUpdateResult { +sealed class FetchUpdateResult { /** * Represents a successful result of a batch update operation. @@ -17,14 +17,14 @@ sealed class BatchFetchUpdateResult { */ data class Success( val data: List>, - ) : BatchFetchUpdateResult() + ) : FetchUpdateResult() /** * Represents an error result of a batch update operation. * * @param error error that occurred during the operation. */ - data class Error(val error: TError) : BatchFetchUpdateResult() + data class Error(val error: TError) : FetchUpdateResult() /** * Represents an unknown error result of a batch update operation. @@ -32,5 +32,5 @@ sealed class BatchFetchUpdateResult { * * @param throwable throwable that occurred during the operation. */ - class UnknownError(val throwable: Throwable) : BatchFetchUpdateResult() + class UnknownError(val throwable: Throwable) : FetchUpdateResult() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt index 054178ce19..ec30a2ea1e 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt @@ -34,12 +34,12 @@ sealed class PaginationStatus { /** * 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] + * For the first batch, [lastResult] is always [FetchResult.Success] * * @param lastResult result of the last batch fetch. */ data class Paginating( - val lastResult: BatchFetchResult, + val lastResult: FetchResult, ) : PaginationStatus() /** 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 901d83f32b..2d9e50832e 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 @@ -1,6 +1,6 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.FetchResult import com.tangem.pagination.BatchListState /** @@ -20,7 +20,7 @@ interface BatchFetcher { * @param request initial request. Will be saved to be used in [fetchNext] requests. * @return result of the fetch operation. */ - suspend fun fetchFirst(request: TRequest): BatchFetchResult + suspend fun fetchFirst(request: TRequest): FetchResult /** * Fetches the next batch of data. @@ -32,6 +32,6 @@ interface BatchFetcher { */ suspend fun fetchNext( overrideRequest: TRequest?, - lastResult: BatchFetchResult, - ): BatchFetchResult + lastResult: FetchResult, + ): FetchResult } \ 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 11af45665d..858411b6a6 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 @@ -1,6 +1,6 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.FetchResult import com.tangem.pagination.exception.EndOfPaginationException import kotlinx.coroutines.flow.MutableStateFlow @@ -18,7 +18,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: (request: Request) -> FetchResult, ) : BatchFetcher { data class Request( @@ -29,7 +29,7 @@ class LimitOffsetBatchFetcher( private val lastRequest = MutableStateFlow?>(null) - override suspend fun fetchFirst(request: TRequest): BatchFetchResult { + override suspend fun fetchFirst(request: TRequest): FetchResult { val req = Request( offset = 0, limit = prefetchDistance, @@ -43,14 +43,14 @@ class LimitOffsetBatchFetcher( override suspend fun fetchNext( overrideRequest: TRequest?, - lastResult: BatchFetchResult, - ): BatchFetchResult { + lastResult: FetchResult, + ): FetchResult { val last = lastRequest.value requireNotNull(last) - val req = if (lastResult is BatchFetchResult.Success) { + val req = if (lastResult is FetchResult.Success) { if (lastResult.last && overrideRequest == null) { - return BatchFetchResult.UnknownError(EndOfPaginationException()) + return FetchResult.UnknownError(EndOfPaginationException()) } Request( From d819a156b2736c03284ff0fce31de38ef0677a1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 19:50:38 +0300 Subject: [PATCH 5/7] Updated on 2026-08-14 --- .../src/main/java/com/tangem/pagination/BatchListSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cfedfbcbf2..0c6c05bd57 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -32,7 +32,7 @@ interface BatchListSource { */ @Suppress("FunctionNaming") fun BatchListSource( - ioDispatcher : CoroutineDispatcher = Dispatchers.IO, + ioDispatcher: CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, From 16eb18b07e6bb285c0e05105b30ba9ffede36462 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 14:39:28 +0300 Subject: [PATCH 6/7] Updated on 2026-08-14 --- .../java/com/tangem/pagination/BatchAction.kt | 24 ++-- .../{FetchResult.kt => BatchFetchResult.kt} | 16 +-- .../com/tangem/pagination/BatchListSource.kt | 128 +++++++++--------- .../com/tangem/pagination/BatchListState.kt | 5 +- .../tangem/pagination/BatchUpdateFetcher.kt | 10 +- .../tangem/pagination/BatchUpdateResult.kt | 27 ++++ .../com/tangem/pagination/BatchingConfig.kt | 12 -- .../com/tangem/pagination/BatchingContext.kt | 6 +- .../tangem/pagination/FetchUpdateResult.kt | 36 ----- .../com/tangem/pagination/PaginationStatus.kt | 29 ++-- .../tangem/pagination/fetcher/BatchFetcher.kt | 12 +- .../fetcher/LimitOffsetBatchFetcher.kt | 25 ++-- 12 files changed, 147 insertions(+), 183 deletions(-) rename core/pagination/src/main/java/com/tangem/pagination/{FetchResult.kt => BatchFetchResult.kt} (55%) create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt 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 813cbe6ba9..20d95b63aa 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -3,41 +3,41 @@ package com.tangem.pagination /** * Action that can be dispatched to [BatchListSource]. * - * @param TRequest type of the request to load batches. + * @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 { +sealed class BatchAction { /** * Action to load the first batch. * - * @param request request to load the first batch. + * @param requestParams request params to load the first batch. */ - data class Reload( - val request: TRequest, - ) : BatchAction() + data class Reload( + val requestParams: TRequestParams, + ) : BatchAction() /** * Action to load the next batch. * - * @param request request to load the next batch with new request. + * @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( - val request: TRequest? = null, - ) : BatchAction() + data class LoadMore( + val requestParams: TRequestParams? = null, + ) : BatchAction() /** * Action to update the batch. * * @param keys keys of the batches to update. - * @param request request to update the batches. + * @param updateRequest request to update the batches. */ class UpdateBatches( val keys: Set, - val request: TUpdate, + val updateRequest: TUpdate, ) : BatchAction() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt similarity index 55% rename from core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt rename to core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index d72c42ac1d..a247310b9c 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -5,9 +5,8 @@ package com.tangem.pagination * Used in [BatchListState]. * * @param TData type of the data. - * @param TError type of the error. */ -sealed class FetchResult { +sealed class BatchFetchResult { /** * Represents a successful result of a batch fetch request. @@ -18,21 +17,14 @@ sealed class FetchResult { data class Success( val data: TData, val last: Boolean = false, - ) : FetchResult() + ) : BatchFetchResult() /** * Represents an error result of a batch fetch request. - * - * @param error error that occurred during the request. - */ - data class Error(val error: TError) : FetchResult() - - /** - * Represents an unknown error result of a batch fetch request. - * Used for unexpected exceptions that occurred in fetch method in BatchFetcher. + * 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 UnknownError(val throwable: Throwable) : FetchResult() + class Error(val throwable: Throwable) : BatchFetchResult() } \ 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 0c6c05bd57..bd521c6f71 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -11,19 +11,18 @@ import kotlinx.coroutines.flow.* * @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. - * @param TError Type of the error that can occur during fetching or updating. * @property state State of the paginated data. * @property updateResults Flow of results of update requests. */ -interface BatchListSource { - val state: StateFlow> - val updateResults: SharedFlow>> +interface BatchListSource { + val state: StateFlow> + val updateResults: SharedFlow>> } /** * Creates a new [BatchListSource] with the provided configuration. * - * @param ioDispatcher Dispatcher for IO operations. + * @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. @@ -31,18 +30,18 @@ interface BatchListSource { * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - ioDispatcher: CoroutineDispatcher = Dispatchers.IO, +fun BatchListSource( + fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, - batchFetcher: BatchFetcher, -): BatchListSource = - BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, null) + batchFetcher: BatchFetcher, +): BatchListSource = + DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null) /** * Creates a new [BatchListSource] with the provided configuration. * - * @param ioDispatcher Dispatcher for IO operations. + * @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. @@ -51,25 +50,25 @@ fun BatchListSource( * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - ioDispatcher: CoroutineDispatcher = Dispatchers.IO, - context: BatchingContext, +fun BatchListSource( + fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, + context: BatchingContext, generateNewKey: suspend (List) -> TKey, - batchFetcher: BatchFetcher, - updateFetcher: BatchUpdateFetcher, -): BatchListSource = - BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, updateFetcher) + batchFetcher: BatchFetcher, + updateFetcher: BatchUpdateFetcher, +): BatchListSource = + DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) -private class BatchListSourceImpl( - private val ioDispatcher: CoroutineDispatcher, - private val context: BatchingContext, +private class DefaultBatchListSource( + private val fetchDispatcher: CoroutineDispatcher, + private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, - private val batchFetcher: BatchFetcher, - private val updateFetcher: BatchUpdateFetcher? = null, -) : BatchListSource { + private val batchFetcher: BatchFetcher, + private val updateFetcher: BatchUpdateFetcher? = null, +) : BatchListSource { - override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) - override val updateResults = MutableSharedFlow>>( + override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) + override val updateResults = MutableSharedFlow>>( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -79,7 +78,7 @@ private class BatchListSourceImpl( private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) - private val lastRequestResult = MutableStateFlow?>(null) + private val lastRequestResult = MutableStateFlow?>(null) private var reloadActionJob: Job? = null private var loadMoreActionJob: Job? = null @@ -105,14 +104,14 @@ private class BatchListSourceImpl( } } - private fun collectActions(action: BatchAction) { + private fun collectActions(action: BatchAction) { when (action) { is BatchAction.Reload -> { // Stop all tasks loadMoreActionJob?.cancel() reloadActionJob?.cancel() stopAllUpdates() - reloadActionJob = scope.launch(ioDispatcher) { + reloadActionJob = scope.launch(fetchDispatcher) { reloadTask(action) } } @@ -121,7 +120,7 @@ private class BatchListSourceImpl( return } - loadMoreActionJob = scope.launch(ioDispatcher) { + loadMoreActionJob = scope.launch(fetchDispatcher) { reloadActionJob?.join() loadMoreTask(action) } @@ -129,7 +128,7 @@ private class BatchListSourceImpl( is BatchAction.UpdateBatches -> { if (updateFetcher == null) return - scope.launch(ioDispatcher) { + scope.launch(fetchDispatcher) { val job = launch(start = CoroutineStart.LAZY) { updateBatchesTask(action) } @@ -172,60 +171,63 @@ private class BatchListSourceImpl( } } - private suspend fun reloadTask(action: BatchAction.Reload) { + private suspend fun reloadTask(action: BatchAction.Reload) { state.value = BatchListState( data = emptyList(), status = PaginationStatus.InitialLoading, ) val res = runCatching { - batchFetcher.fetchFirst(action.request) - }.getOrElse { FetchResult.UnknownError(it) } + batchFetcher.fetchFirst(action.requestParams) + }.getOrElse { BatchFetchResult.Error(it) } - state.value = if (res is FetchResult.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) - }, - ) - } else { - BatchListState( - data = emptyList(), - status = PaginationStatus.InitialLoadingError( - error = (res as? FetchResult.Error)?.error, - ), - ) + 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) { + private suspend fun loadMoreTask(action: BatchAction.LoadMore) { val status = state.value.status if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return - if (status is PaginationStatus.EndOfPagination && action.request == null) 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.request, lastResult) - }.getOrElse { FetchResult.UnknownError(it) } + batchFetcher.fetchNext(action.requestParams, lastResult) + }.getOrElse { BatchFetchResult.Error(it) } lastRequestResult.value = lastResult state.update { currentState -> - if (res is FetchResult.Success) { + if (res is BatchFetchResult.Success) { val newBatch = Batch( key = generateNewKey(currentState.data.map { it.key }), data = res.data, @@ -255,10 +257,10 @@ private class BatchListSourceImpl( val result = updateFetcher.fetchUpdate( toUpdate = batchesToUpdate, - updateRequest = action.request, + updateRequest = action.updateRequest, ) - if (result is FetchUpdateResult.Success) { + if (result is BatchUpdateResult.Success) { state.update { currentState -> val resMap = result.data.associateBy { it.key } currentState.copy( @@ -269,7 +271,7 @@ private class BatchListSourceImpl( } } - updateResults.emit(action.request to result) + updateResults.emit(action.updateRequest to result) } private fun stopAllUpdates() { diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt index 525458362f..b53ba23dc0 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -5,14 +5,13 @@ package com.tangem.pagination * * @param TKey type of the key of the batch. * @param TData type of the data. - * @param TError type of the error. * * @property data list of loaded batches. * @property status current status of the pagination. * * @see BatchListSource */ -data class BatchListState( +data class BatchListState( val data: List>, - val status: PaginationStatus, + val status: PaginationStatus, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt index 1cf7e10a91..704c416316 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -6,20 +6,18 @@ package com.tangem.pagination * * @param TKey type of the key. * @param TData type of the data. - * @param TError type of the error. * @param TUpdate type of the update request. */ -interface BatchUpdateFetcher { +fun interface BatchUpdateFetcher { /** * 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>, - updateRequest: TUpdate, - ): FetchUpdateResult + suspend fun fetchUpdate(toUpdate: List>, updateRequest: TUpdate): BatchUpdateResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt new file mode 100644 index 0000000000..c2e00fad9c --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt @@ -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 { + + /** + * Represents a successful result of a batch update operation. + * + * @param data fetched data. + */ + data class Success( + val data: List>, + ) : BatchUpdateResult() + + /** + * Represents an error result of a batch update operation. + * + * @param error error that occurred during the operation. + */ + class Error(val throwable: Throwable) : BatchUpdateResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt deleted file mode 100644 index 8789d11405..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.pagination - -/** - * Configuration for batching. - * - * @param batchSize size of the batch. - * @param prefetchDistance number of items to fetch for the first batch. - */ -data class BatchingConfig( - val batchSize: Int, - val prefetchDistance: Int = batchSize, -) \ No newline at end of file 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 37b6bd2c73..fb9672d365 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow /** * Context for working with [BatchListSource]. * - * @param TRequest type of the request. + * @param TRequestParams type of the request. * @param TKey type of the key. * @param TUpdate type of the update request. * @@ -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/FetchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt deleted file mode 100644 index d575044a1f..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt +++ /dev/null @@ -1,36 +0,0 @@ -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. - * @param TError type of the error. - */ -sealed class FetchUpdateResult { - - /** - * Represents a successful result of a batch update operation. - * - * @param data fetched data. - */ - data class Success( - val data: List>, - ) : FetchUpdateResult() - - /** - * Represents an error result of a batch update operation. - * - * @param error error that occurred during the operation. - */ - data class Error(val error: TError) : FetchUpdateResult() - - /** - * Represents an unknown error result of a batch update operation. - * Used for unexpected exceptions that occurred in `fetchUpdate` method in [BatchUpdateFetcher]. - * - * @param throwable throwable that occurred during the operation. - */ - class UnknownError(val throwable: Throwable) : FetchUpdateResult() -} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt index ec30a2ea1e..5e30875c6a 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt @@ -3,54 +3,53 @@ package com.tangem.pagination /** * Status of the pagination. * - * @param T type of the data. - * @param E type of the error. + * @param TData type of the data. * * @see BatchListState */ -sealed class PaginationStatus { +sealed class PaginationStatus { /** * Represents that there is no data. Used when the list of batches is empty. * The initial state of the pagination. */ - data object None : PaginationStatus() + data object None : PaginationStatus() /** * 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() + data object InitialLoading : PaginationStatus() /** * Represents that the first batch was loaded with an error. * * @param error error that occurred during the initial loading. */ - data class InitialLoadingError( - val error: E?, - ) : PaginationStatus() + data class InitialLoadingError( + val throwable: Throwable, + ) : PaginationStatus() /** * 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 [FetchResult.Success] + * For the first batch, [lastResult] is always [BatchFetchResult.Success] * * @param lastResult result of the last batch fetch. */ - data class Paginating( - val lastResult: FetchResult, - ) : PaginationStatus() + data class Paginating( + val lastResult: BatchFetchResult, + ) : PaginationStatus() /** * Represents that the next batch is loading. * Used when the next batch is being loaded. */ - data object NextBatchLoading : PaginationStatus() + data object NextBatchLoading : PaginationStatus() /** * Represents that the source has no more batches to load. - * The next [BatchAction.LoadMore] with [BatchAction.LoadMore.request] = null will be ignored. + * The next [BatchAction.LoadMore] with [BatchAction.LoadMore.requestParams] = null will be ignored. */ - data object EndOfPagination : PaginationStatus() + data object EndOfPagination : PaginationStatus() } \ 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 2d9e50832e..98358257b5 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 @@ -1,6 +1,6 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.FetchResult +import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchListState /** @@ -8,11 +8,10 @@ import com.tangem.pagination.BatchListState * * @param TRequest type of the request. * @param TData type of the data. - * @param TError type of the error. * * @see BatchListState */ -interface BatchFetcher { +interface BatchFetcher { /** * Fetches the first batch of data. @@ -20,7 +19,7 @@ interface BatchFetcher { * @param request initial request. Will be saved to be used in [fetchNext] requests. * @return result of the fetch operation. */ - suspend fun fetchFirst(request: TRequest): FetchResult + suspend fun fetchFirst(request: TRequest): BatchFetchResult /** * Fetches the next batch of data. @@ -30,8 +29,5 @@ interface BatchFetcher { * @param lastResult result of the last fetch operation. * @return result of the fetch operation. */ - suspend fun fetchNext( - overrideRequest: TRequest?, - lastResult: FetchResult, - ): FetchResult + suspend fun fetchNext(overrideRequest: TRequest?, 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 858411b6a6..6a321a61e9 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 @@ -1,25 +1,24 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.FetchResult +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 TRequest type of the request. + * @param TRequestParams type of the request params. * @param TData type of the data. - * @param TError type of the error. * * @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( +class LimitOffsetBatchFetcher( private val prefetchDistance: Int, private val batchSize: Int, - private val fetch: (request: Request) -> FetchResult, -) : BatchFetcher { + private val fetch: (request: Request) -> BatchFetchResult, +) : BatchFetcher { data class Request( val limit: Int, @@ -27,9 +26,9 @@ class LimitOffsetBatchFetcher( val request: TRequest, ) - private val lastRequest = MutableStateFlow?>(null) + private val lastRequest = MutableStateFlow?>(null) - override suspend fun fetchFirst(request: TRequest): FetchResult { + override suspend fun fetchFirst(request: TRequestParams): BatchFetchResult { val req = Request( offset = 0, limit = prefetchDistance, @@ -42,15 +41,15 @@ class LimitOffsetBatchFetcher( } override suspend fun fetchNext( - overrideRequest: TRequest?, - lastResult: FetchResult, - ): FetchResult { + overrideRequest: TRequestParams?, + lastResult: BatchFetchResult, + ): BatchFetchResult { val last = lastRequest.value requireNotNull(last) - val req = if (lastResult is FetchResult.Success) { + val req = if (lastResult is BatchFetchResult.Success) { if (lastResult.last && overrideRequest == null) { - return FetchResult.UnknownError(EndOfPaginationException()) + return BatchFetchResult.Error(EndOfPaginationException()) } Request( From 7dd5e87cfd3c7f0615792d8e407aae8babfa8295 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 17:10:03 +0300 Subject: [PATCH 7/7] 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