Updated on 2026-08-14
This commit is contained in:
parent
16eb18b07e
commit
7dd5e87cfd
5 changed files with 61 additions and 44 deletions
|
|
@ -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>()
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue