Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-03 19:09:34 +03:00
parent 5c51d8c3f1
commit dc97dbd91f
8 changed files with 142 additions and 103 deletions

View file

@ -14,9 +14,9 @@ sealed class BatchAction<TRequest, TKey, TUpdate> {
*
* @param request request to load the first batch.
*/
data class Reload<R>(
val request: R,
) : BatchAction<R, Nothing, Nothing>()
data class Reload<TRequest : Any>(
val request: TRequest,
) : BatchAction<TRequest, Nothing, Nothing>()
/**
* Action to load the next batch.
@ -25,9 +25,9 @@ sealed class BatchAction<TRequest, TKey, TUpdate> {
* 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<R>(
val request: R? = null,
) : BatchAction<R, Nothing, Nothing>()
data class LoadMore<TRequest : Any>(
val request: TRequest? = null,
) : BatchAction<TRequest, Nothing, Nothing>()
/**
* Action to update the batch.
@ -35,10 +35,10 @@ sealed class BatchAction<TRequest, TKey, TUpdate> {
* @param keys keys of the batches to update.
* @param request request to update the batches.
*/
class UpdateBatches<K, U>(
val keys: Set<K>,
val request: U,
) : BatchAction<Nothing, K, U>()
class UpdateBatches<TKey, TUpdate>(
val keys: Set<TKey>,
val request: TUpdate,
) : BatchAction<Nothing, TKey, TUpdate>()
/**
* Action to cancel the current batch loading.
@ -55,7 +55,7 @@ sealed class BatchAction<TRequest, TKey, TUpdate> {
*
* @param predicate predicate to check if the update request should be cancelled.
*/
class CancelUpdates<K, U>(
val predicate: (UpdateBatches<K, U>) -> Boolean,
) : BatchAction<Nothing, K, U>()
class CancelUpdates<TKey, TUpdate>(
val predicate: (UpdateBatches<TKey, TUpdate>) -> Boolean,
) : BatchAction<Nothing, TKey, TUpdate>()
}

View file

@ -29,9 +29,10 @@ sealed class BatchFetchResult<out TData, out TError> {
/**
* 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<Nothing, Nothing>()
}

View file

@ -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<TRequest, TData, TError> {
/**
* Fetches a batch of data.
*
* @param request request to fetch the data.
* @return result of the fetch operation.
*/
suspend fun fetch(request: BatchRequest<TRequest>): BatchFetchResult<TData, TError>
}

View file

@ -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<TKey, TData, TUpdate, TError> {
* @return New instance of [BatchListSource].
*/
@Suppress("FunctionNaming")
fun <TKey, TData, TRequest, TError> BatchListSource(
config: BatchingConfig,
fun <TKey, TData, TRequest : Any, TError> BatchListSource(
context: BatchingContext<TRequest, TKey, Nothing>,
generateNewKey: suspend (List<TKey>) -> TKey,
batchFetcher: BatchFetcher<TRequest, TData, TError>,
): BatchListSource<TKey, TData, Nothing, TError> =
BatchListSourceImpl(config, context, generateNewKey, batchFetcher, null)
): BatchListSource<TKey, TData, Nothing, TError> = BatchListSourceImpl(context, generateNewKey, batchFetcher, null)
/**
* Creates a new [BatchListSource] with the provided configuration.
@ -50,17 +49,15 @@ fun <TKey, TData, TRequest, TError> BatchListSource(
* @return New instance of [BatchListSource].
*/
@Suppress("FunctionNaming")
fun <TKey, TData, TUpdate, TRequest, TError> BatchListSource(
config: BatchingConfig,
fun <TKey, TData, TUpdate, TRequest : Any, TError> BatchListSource(
context: BatchingContext<TRequest, TKey, TUpdate>,
generateNewKey: suspend (List<TKey>) -> TKey,
batchFetcher: BatchFetcher<TRequest, TData, TError>,
updateFetcher: BatchUpdateFetcher<TKey, TData, TError, TUpdate>,
): BatchListSource<TKey, TData, TUpdate, TError> =
BatchListSourceImpl(config, context, generateNewKey, batchFetcher, updateFetcher)
BatchListSourceImpl(context, generateNewKey, batchFetcher, updateFetcher)
private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest, TError>(
private val config: BatchingConfig,
private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest : Any, TError>(
private val context: BatchingContext<TRequest, TKey, TUpdate>,
private val generateNewKey: suspend (List<TKey>) -> TKey,
private val batchFetcher: BatchFetcher<TRequest, TData, TError>,
@ -78,7 +75,7 @@ private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest, TError>(
private val waitingUpdateJobs =
MutableStateFlow<List<Pair<BatchAction.UpdateBatches<TKey, TUpdate>, Job>>>(emptyList())
private val lastRequest = MutableStateFlow<BatchRequest<TRequest>?>(null)
private val lastRequestResult = MutableStateFlow<BatchFetchResult<TData, TError>?>(null)
private var reloadActionJob: Job? = null
private var loadMoreActionJob: Job? = null
@ -88,10 +85,9 @@ private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest, TError>(
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<TKey, TData, TUpdate, TRequest, TError>(
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<TKey, TData, TUpdate, TRequest, TError>(
)
}
lastRequest.value = request
lastRequestResult.value = res
}
private suspend fun loadMoreTask(action: BatchAction.LoadMore<TRequest>) {
val status = state.value.status
val lastReq = lastRequest.value
val request: BatchRequest<TRequest> = 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) {

View file

@ -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<TRequest>(
val offset: Int,
val limit: Int,
val data: TRequest,
)

View file

@ -0,0 +1,6 @@
package com.tangem.pagination.exception
/**
* Exception that is thrown when there are no more items to fetch.
*/
class EndOfPaginationException : IllegalStateException()

View file

@ -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<TRequest : Any, TData, TError> {
/**
* 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<TData, TError>
/**
* 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<TData, TError>,
): BatchFetchResult<TData, TError>
}

View file

@ -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<TRequest : Any, TData, TError>(
private val prefetchDistance: Int,
private val batchSize: Int,
private val fetch: (request: Request<TRequest>) -> BatchFetchResult<TData, TError>,
) : BatchFetcher<TRequest, TData, TError> {
data class Request<TRequest>(
val limit: Int,
val offset: Int,
val request: TRequest,
)
private val lastRequest = MutableStateFlow<Request<TRequest>?>(null)
override suspend fun fetchFirst(request: TRequest): BatchFetchResult<TData, TError> {
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<TData, TError>,
): BatchFetchResult<TData, TError> {
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
}
}