Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-04 17:10:03 +03:00
parent 16eb18b07e
commit 7dd5e87cfd
5 changed files with 61 additions and 44 deletions

View file

@ -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<TRequestParams, TKey, TUpdate> {
sealed class BatchAction<TKey, TRequestParams, TUpdate> {
/**
* Action to load the first batch.
@ -16,7 +16,7 @@ sealed class BatchAction<TRequestParams, TKey, TUpdate> {
*/
data class Reload<TRequestParams : Any>(
val requestParams: TRequestParams,
) : BatchAction<TRequestParams, Nothing, Nothing>()
) : BatchAction<Nothing, TRequestParams, Nothing>()
/**
* Action to load the next batch.
@ -27,7 +27,7 @@ sealed class BatchAction<TRequestParams, TKey, TUpdate> {
*/
data class LoadMore<TRequestParams : Any>(
val requestParams: TRequestParams? = null,
) : BatchAction<TRequestParams, Nothing, Nothing>()
) : BatchAction<Nothing, TRequestParams, Nothing>()
/**
* Action to update the batch.
@ -38,7 +38,7 @@ sealed class BatchAction<TRequestParams, TKey, TUpdate> {
class UpdateBatches<TKey, TUpdate>(
val keys: Set<TKey>,
val updateRequest: TUpdate,
) : BatchAction<Nothing, TKey, TUpdate>()
) : BatchAction<TKey, Nothing, TUpdate>()
/**
* Action to cancel the current batch loading.
@ -57,5 +57,5 @@ sealed class BatchAction<TRequestParams, TKey, TUpdate> {
*/
class CancelUpdates<TKey, TUpdate>(
val predicate: (UpdateBatches<TKey, TUpdate>) -> Boolean,
) : BatchAction<Nothing, TKey, TUpdate>()
) : BatchAction<TKey, Nothing, TUpdate>()
}

View file

@ -30,11 +30,11 @@ interface BatchListSource<TKey, TData, TUpdate> {
* @return New instance of [BatchListSource].
*/
@Suppress("FunctionNaming")
fun <TKey, TData, TRequest : Any> BatchListSource(
fun <TKey, TData, TRequestParams : Any> BatchListSource(
fetchDispatcher: CoroutineDispatcher = Dispatchers.IO,
context: BatchingContext<TRequest, TKey, Nothing>,
context: BatchingContext<TKey, TRequestParams, Nothing>,
generateNewKey: suspend (List<TKey>) -> TKey,
batchFetcher: BatchFetcher<TRequest, TData>,
batchFetcher: BatchFetcher<TRequestParams, TData>,
): BatchListSource<TKey, TData, Nothing> =
DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null)
@ -50,18 +50,18 @@ fun <TKey, TData, TRequest : Any> BatchListSource(
* @return New instance of [BatchListSource].
*/
@Suppress("FunctionNaming")
fun <TKey, TData, TUpdate, TRequestParams : Any> BatchListSource(
fun <TKey, TData, TRequestParams : Any, TUpdate> BatchListSource(
fetchDispatcher: CoroutineDispatcher = Dispatchers.IO,
context: BatchingContext<TRequestParams, TKey, TUpdate>,
context: BatchingContext<TKey, TRequestParams, TUpdate>,
generateNewKey: suspend (List<TKey>) -> TKey,
batchFetcher: BatchFetcher<TRequestParams, TData>,
updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>,
): BatchListSource<TKey, TData, TUpdate> =
DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher)
private class DefaultBatchListSource<TKey, TData, TUpdate, TRequestParams : Any>(
private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>(
private val fetchDispatcher: CoroutineDispatcher,
private val context: BatchingContext<TRequestParams, TKey, TUpdate>,
private val context: BatchingContext<TKey, TRequestParams, TUpdate>,
private val generateNewKey: suspend (List<TKey>) -> TKey,
private val batchFetcher: BatchFetcher<TRequestParams, TData>,
private val updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>? = null,
@ -104,7 +104,7 @@ private class DefaultBatchListSource<TKey, TData, TUpdate, TRequestParams : Any>
}
}
private fun collectActions(action: BatchAction<TRequestParams, TKey, TUpdate>) {
private fun collectActions(action: BatchAction<TKey, TRequestParams, TUpdate>) {
when (action) {
is BatchAction.Reload -> {
// Stop all tasks
@ -129,6 +129,8 @@ private class DefaultBatchListSource<TKey, TData, TUpdate, TRequestParams : Any>
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<TKey, TData, TUpdate, TRequestParams : Any>
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<TKey, TData, TUpdate, TRequestParams : Any>
private suspend fun loadMoreTask(action: BatchAction.LoadMore<TRequestParams>) {
val status = state.value.status
// Skip the action if the state is not ready to continue pagination.
// Two options are acceptable:
// 1. The Source is ready to load next page with the same or different request params.
// 2. The Source has reached the end of pagination, but there is another request
// that can possibly load the next page and continue the pagination
if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return
if (status is PaginationStatus.EndOfPagination && action.requestParams == null) return
@ -227,24 +238,27 @@ private class DefaultBatchListSource<TKey, TData, TUpdate, TRequestParams : Any>
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),
)
}
}
}
}

View file

@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.Flow
*
* @see BatchListSource
*/
class BatchingContext<TRequestParams, TKey, TUpdate>(
val actionsFlow: Flow<BatchAction<TRequestParams, TKey, TUpdate>>,
class BatchingContext<TKey, TRequestParams, TUpdate>(
val actionsFlow: Flow<BatchAction<TKey, TRequestParams, TUpdate>>,
val coroutineScope: CoroutineScope,
)

View file

@ -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<TRequest : Any, TData> {
interface BatchFetcher<TRequestParams : Any, TData> {
/**
* 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<TData>
suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<TData>
/**
* 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<TData>): BatchFetchResult<TData>
suspend fun fetchNext(
overrideRequestParams: TRequestParams?,
lastResult: BatchFetchResult<TData>,
): BatchFetchResult<TData>
}

View file

@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
private val prefetchDistance: Int,
private val batchSize: Int,
private val fetch: (request: Request<TRequestParams>) -> BatchFetchResult<TData>,
private val fetch: suspend (request: Request<TRequestParams>) -> BatchFetchResult<TData>,
) : BatchFetcher<TRequestParams, TData> {
data class Request<TRequest>(
@ -28,11 +28,11 @@ class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
override suspend fun fetchFirst(request: TRequestParams): BatchFetchResult<TData> {
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<TData> {
val req = Request(
offset = 0,
limit = prefetchDistance,
request = request,
request = requestParams,
)
val res = fetch(req)
@ -41,21 +41,21 @@ class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
}
override suspend fun fetchNext(
overrideRequest: TRequestParams?,
overrideRequestParams: TRequestParams?,
lastResult: BatchFetchResult<TData>,
): BatchFetchResult<TData> {
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