Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-04 14:39:28 +03:00
parent d819a156b2
commit 16eb18b07e
12 changed files with 147 additions and 183 deletions

View file

@ -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<TRequest, TKey, TUpdate> {
sealed class BatchAction<TRequestParams, TKey, TUpdate> {
/**
* 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<TRequest : Any>(
val request: TRequest,
) : BatchAction<TRequest, Nothing, Nothing>()
data class Reload<TRequestParams : Any>(
val requestParams: TRequestParams,
) : BatchAction<TRequestParams, Nothing, Nothing>()
/**
* 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<TRequest : Any>(
val request: TRequest? = null,
) : BatchAction<TRequest, Nothing, Nothing>()
data class LoadMore<TRequestParams : Any>(
val requestParams: TRequestParams? = null,
) : BatchAction<TRequestParams, Nothing, Nothing>()
/**
* 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<TKey, TUpdate>(
val keys: Set<TKey>,
val request: TUpdate,
val updateRequest: TUpdate,
) : BatchAction<Nothing, TKey, TUpdate>()
/**

View file

@ -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<out TData, out TError> {
sealed class BatchFetchResult<out TData> {
/**
* Represents a successful result of a batch fetch request.
@ -18,21 +17,14 @@ sealed class FetchResult<out TData, out TError> {
data class Success<TData>(
val data: TData,
val last: Boolean = false,
) : FetchResult<TData, Nothing>()
) : BatchFetchResult<TData>()
/**
* Represents an error result of a batch fetch request.
*
* @param error error that occurred during the request.
*/
data class Error<TError>(val error: TError) : FetchResult<Nothing, TError>()
/**
* 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<Nothing, Nothing>()
class Error(val throwable: Throwable) : BatchFetchResult<Nothing>()
}

View file

@ -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<TKey, TData, TUpdate, TError> {
val state: StateFlow<BatchListState<TKey, TData, TError>>
val updateResults: SharedFlow<Pair<TUpdate, FetchUpdateResult<TKey, TData, TError>>>
interface BatchListSource<TKey, TData, TUpdate> {
val state: StateFlow<BatchListState<TKey, TData>>
val updateResults: SharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>
}
/**
* 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<TKey, TData, TUpdate, TError> {
* @return New instance of [BatchListSource].
*/
@Suppress("FunctionNaming")
fun <TKey, TData, TRequest : Any, TError> BatchListSource(
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
fun <TKey, TData, TRequest : Any> BatchListSource(
fetchDispatcher: CoroutineDispatcher = Dispatchers.IO,
context: BatchingContext<TRequest, TKey, Nothing>,
generateNewKey: suspend (List<TKey>) -> TKey,
batchFetcher: BatchFetcher<TRequest, TData, TError>,
): BatchListSource<TKey, TData, Nothing, TError> =
BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, null)
batchFetcher: BatchFetcher<TRequest, TData>,
): BatchListSource<TKey, TData, Nothing> =
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 <TKey, TData, TRequest : Any, TError> BatchListSource(
* @return New instance of [BatchListSource].
*/
@Suppress("FunctionNaming")
fun <TKey, TData, TUpdate, TRequest : Any, TError> BatchListSource(
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
context: BatchingContext<TRequest, TKey, TUpdate>,
fun <TKey, TData, TUpdate, TRequestParams : Any> BatchListSource(
fetchDispatcher: CoroutineDispatcher = Dispatchers.IO,
context: BatchingContext<TRequestParams, TKey, TUpdate>,
generateNewKey: suspend (List<TKey>) -> TKey,
batchFetcher: BatchFetcher<TRequest, TData, TError>,
updateFetcher: BatchUpdateFetcher<TKey, TData, TError, TUpdate>,
): BatchListSource<TKey, TData, TUpdate, TError> =
BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, updateFetcher)
batchFetcher: BatchFetcher<TRequestParams, TData>,
updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>,
): BatchListSource<TKey, TData, TUpdate> =
DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher)
private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest : Any, TError>(
private val ioDispatcher: CoroutineDispatcher,
private val context: BatchingContext<TRequest, TKey, TUpdate>,
private class DefaultBatchListSource<TKey, TData, TUpdate, TRequestParams : Any>(
private val fetchDispatcher: CoroutineDispatcher,
private val context: BatchingContext<TRequestParams, TKey, TUpdate>,
private val generateNewKey: suspend (List<TKey>) -> TKey,
private val batchFetcher: BatchFetcher<TRequest, TData, TError>,
private val updateFetcher: BatchUpdateFetcher<TKey, TData, TError, TUpdate>? = null,
) : BatchListSource<TKey, TData, TUpdate, TError> {
private val batchFetcher: BatchFetcher<TRequestParams, TData>,
private val updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>? = null,
) : BatchListSource<TKey, TData, TUpdate> {
override val state = MutableStateFlow(BatchListState<TKey, TData, TError>(emptyList(), PaginationStatus.None))
override val updateResults = MutableSharedFlow<Pair<TUpdate, FetchUpdateResult<TKey, TData, TError>>>(
override val state = MutableStateFlow(BatchListState<TKey, TData>(emptyList(), PaginationStatus.None))
override val updateResults = MutableSharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
@ -79,7 +78,7 @@ private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest : Any, TError>(
private val waitingUpdateJobs =
MutableStateFlow<List<Pair<BatchAction.UpdateBatches<TKey, TUpdate>, Job>>>(emptyList())
private val lastRequestResult = MutableStateFlow<FetchResult<TData, TError>?>(null)
private val lastRequestResult = MutableStateFlow<BatchFetchResult<TData>?>(null)
private var reloadActionJob: Job? = null
private var loadMoreActionJob: Job? = null
@ -105,14 +104,14 @@ private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest : Any, TError>(
}
}
private fun collectActions(action: BatchAction<TRequest, TKey, TUpdate>) {
private fun collectActions(action: BatchAction<TRequestParams, TKey, TUpdate>) {
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<TKey, TData, TUpdate, TRequest : Any, TError>(
return
}
loadMoreActionJob = scope.launch(ioDispatcher) {
loadMoreActionJob = scope.launch(fetchDispatcher) {
reloadActionJob?.join()
loadMoreTask(action)
}
@ -129,7 +128,7 @@ private class BatchListSourceImpl<TKey, TData, TUpdate, TRequest : Any, TError>(
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<TKey, TData, TUpdate, TRequest : Any, TError>(
}
}
private suspend fun reloadTask(action: BatchAction.Reload<TRequest>) {
private suspend fun reloadTask(action: BatchAction.Reload<TRequestParams>) {
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<TRequest>) {
private suspend fun loadMoreTask(action: BatchAction.LoadMore<TRequestParams>) {
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<TKey, TData, TUpdate, TRequest : Any, TError>(
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<TKey, TData, TUpdate, TRequest : Any, TError>(
}
}
updateResults.emit(action.request to result)
updateResults.emit(action.updateRequest to result)
}
private fun stopAllUpdates() {

View file

@ -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<TKey, TData, TError>(
data class BatchListState<TKey, TData>(
val data: List<Batch<TKey, TData>>,
val status: PaginationStatus<TData, TError>,
val status: PaginationStatus<TData>,
)

View file

@ -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<TKey, TData, TError, TUpdate> {
fun interface BatchUpdateFetcher<TKey, TData, TUpdate> {
/**
* 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<Batch<TKey, TData>>,
updateRequest: TUpdate,
): FetchUpdateResult<TKey, TData, TError>
suspend fun fetchUpdate(toUpdate: List<Batch<TKey, TData>>, updateRequest: TUpdate): BatchUpdateResult<TKey, TData>
}

View file

@ -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<out TKey, out TData> {
/**
* Represents a successful result of a batch update operation.
*
* @param data fetched data.
*/
data class Success<TKey, TData>(
val data: List<Batch<TKey, TData>>,
) : BatchUpdateResult<TKey, TData>()
/**
* Represents an error result of a batch update operation.
*
* @param error error that occurred during the operation.
*/
class Error(val throwable: Throwable) : BatchUpdateResult<Nothing, Nothing>()
}

View file

@ -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,
)

View file

@ -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<TRequest, TKey, TUpdate>(
val actionsFlow: Flow<BatchAction<TRequest, TKey, TUpdate>>,
class BatchingContext<TRequestParams, TKey, TUpdate>(
val actionsFlow: Flow<BatchAction<TRequestParams, TKey, TUpdate>>,
val coroutineScope: CoroutineScope,
)

View file

@ -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<out TKey, out TData, out TError> {
/**
* Represents a successful result of a batch update operation.
*
* @param data fetched data.
*/
data class Success<TKey, TData>(
val data: List<Batch<TKey, TData>>,
) : FetchUpdateResult<TKey, TData, Nothing>()
/**
* Represents an error result of a batch update operation.
*
* @param error error that occurred during the operation.
*/
data class Error<TError>(val error: TError) : FetchUpdateResult<Nothing, Nothing, TError>()
/**
* 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<Nothing, Nothing, Nothing>()
}

View file

@ -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<out T, out E> {
sealed class PaginationStatus<out TData> {
/**
* Represents that there is no data. Used when the list of batches is empty.
* The initial state of the pagination.
*/
data object None : PaginationStatus<Nothing, Nothing>()
data object None : PaginationStatus<Nothing>()
/**
* 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<Nothing, Nothing>()
data object InitialLoading : PaginationStatus<Nothing>()
/**
* Represents that the first batch was loaded with an error.
*
* @param error error that occurred during the initial loading.
*/
data class InitialLoadingError<T, E>(
val error: E?,
) : PaginationStatus<T, E>()
data class InitialLoadingError(
val throwable: Throwable,
) : PaginationStatus<Nothing>()
/**
* 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<out T, out E>(
val lastResult: FetchResult<T, E>,
) : PaginationStatus<T, E>()
data class Paginating<out TData>(
val lastResult: BatchFetchResult<TData>,
) : PaginationStatus<TData>()
/**
* Represents that the next batch is loading.
* Used when the next batch is being loaded.
*/
data object NextBatchLoading : PaginationStatus<Nothing, Nothing>()
data object NextBatchLoading : PaginationStatus<Nothing>()
/**
* 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<Nothing, Nothing>()
data object EndOfPagination : PaginationStatus<Nothing>()
}

View file

@ -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<TRequest : Any, TData, TError> {
interface BatchFetcher<TRequest : Any, TData> {
/**
* Fetches the first batch of data.
@ -20,7 +19,7 @@ interface BatchFetcher<TRequest : Any, TData, TError> {
* @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<TData, TError>
suspend fun fetchFirst(request: TRequest): BatchFetchResult<TData>
/**
* Fetches the next batch of data.
@ -30,8 +29,5 @@ interface BatchFetcher<TRequest : Any, TData, TError> {
* @param lastResult result of the last fetch operation.
* @return result of the fetch operation.
*/
suspend fun fetchNext(
overrideRequest: TRequest?,
lastResult: FetchResult<TData, TError>,
): FetchResult<TData, TError>
suspend fun fetchNext(overrideRequest: TRequest?, lastResult: BatchFetchResult<TData>): BatchFetchResult<TData>
}

View file

@ -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<TRequest : Any, TData, TError>(
class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
private val prefetchDistance: Int,
private val batchSize: Int,
private val fetch: (request: Request<TRequest>) -> FetchResult<TData, TError>,
) : BatchFetcher<TRequest, TData, TError> {
private val fetch: (request: Request<TRequestParams>) -> BatchFetchResult<TData>,
) : BatchFetcher<TRequestParams, TData> {
data class Request<TRequest>(
val limit: Int,
@ -27,9 +26,9 @@ class LimitOffsetBatchFetcher<TRequest : Any, TData, TError>(
val request: TRequest,
)
private val lastRequest = MutableStateFlow<Request<TRequest>?>(null)
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
override suspend fun fetchFirst(request: TRequest): FetchResult<TData, TError> {
override suspend fun fetchFirst(request: TRequestParams): BatchFetchResult<TData> {
val req = Request(
offset = 0,
limit = prefetchDistance,
@ -42,15 +41,15 @@ class LimitOffsetBatchFetcher<TRequest : Any, TData, TError>(
}
override suspend fun fetchNext(
overrideRequest: TRequest?,
lastResult: FetchResult<TData, TError>,
): FetchResult<TData, TError> {
overrideRequest: TRequestParams?,
lastResult: BatchFetchResult<TData>,
): BatchFetchResult<TData> {
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(