Updated on 2026-08-14
This commit is contained in:
parent
5c51d8c3f1
commit
dc97dbd91f
8 changed files with 142 additions and 103 deletions
|
|
@ -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>()
|
||||
}
|
||||
|
|
@ -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>()
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.pagination.exception
|
||||
|
||||
/**
|
||||
* Exception that is thrown when there are no more items to fetch.
|
||||
*/
|
||||
class EndOfPaginationException : IllegalStateException()
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue