package dev.inmo.micro_utils.pagination.compose import androidx.compose.runtime.* import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.micro_utils.pagination.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock /** * Context for managing infinite pagination in a Compose UI. * * @param T The type of the data being paginated. * @property iterationState Holds the current pagination state and iteration count. * @property dataState Stores the loaded data, initially null. * @constructor Internal constructor to initialize pagination. * @param page Initial page number. * @param size Number of items per page. */ class InfinityPagedComponentContext internal constructor( page: Int, size: Int, private val scope: CoroutineScope, private val loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult ) { internal val startPage = SimplePagination(page, size) internal val latestLoadedPage = MutableRedeliverStateFlow?>(null) internal val dataState = MutableRedeliverStateFlow?>(null) internal var loadingJob: Job? = null internal val loadingMutex = Mutex() /** * Loads the next page of data. If the current page is the last one, the function returns early. */ fun loadNext(): Job { return scope.launchLoggingDropExceptions { loadingMutex.withLock { if (latestLoadedPage.value ?.isLastPage == true) return@launchLoggingDropExceptions loadingJob = loadingJob ?: scope.launchLoggingDropExceptions { runCatching { loader(latestLoadedPage.value ?.nextPage() ?: startPage) }.onSuccess { latestLoadedPage.value = it dataState.value = (dataState.value ?: emptyList()) + it.results } loadingMutex.withLock { loadingJob = null } } loadingJob } ?.join() } } /** * Reloads the pagination from the first page, clearing previously loaded data. */ fun reload(): Job { latestLoadedPage.value = null dataState.value = null return loadNext() } } /** * Creates and remembers an [InfinityPagedComponentContext] for managing infinite pagination in a Compose UI. * This function is used to create a persistent pagination context that survives recompositions. * * @param size Number of items to load per page. * @param page Initial page number to start pagination from (defaults to 0). * @param scope [CoroutineScope] to launch pagination operations in. If not provided, a new scope will be created * using [rememberCoroutineScope]. * @param loader Suspended function that loads paginated data. Receives the current pagination context and * pagination parameters, and returns a [PaginationResult] containing the loaded data. * @return An [InfinityPagedComponentContext] instance that manages the pagination state and operations. */ @Composable fun rememberInfinityPagedComponentContext( size: Int, page: Int = 0, scope: CoroutineScope = rememberCoroutineScope(), doReloadInInit: Boolean = true, loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult ): InfinityPagedComponentContext { val context = remember { InfinityPagedComponentContext( page = page, size = size, scope = scope, loader = loader ) } LaunchedEffect(context) { if (doReloadInInit) { context.reload() } } return context } /** * Composable function for managing an infinitely paged component. * * @param T The type of the paginated data. * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will * receive null as `it` parameter */ @Composable fun InfinityPagedComponent( context: InfinityPagedComponentContext, block: @Composable InfinityPagedComponentContext.(List?) -> Unit ) { val dataState = context.dataState.collectAsState() context.block(dataState.value) } /** * Composable function for managing an infinitely paged component. * * @param T The type of the paginated data. * @param page Initial page number. * @param size Number of items per page. * @param loader Suspended function that loads paginated data. * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will * receive null as `it` parameter */ @Composable fun InfinityPagedComponent( page: Int, size: Int, loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult, predefinedScope: CoroutineScope? = null, block: @Composable InfinityPagedComponentContext.(List?) -> Unit ) { val scope = predefinedScope ?: rememberCoroutineScope() val context = rememberInfinityPagedComponentContext(page = page, size = size, scope = scope, loader = loader) InfinityPagedComponent(context, block) } /** * Overloaded composable function for an infinitely paged component. * * @param T The type of the paginated data. * @param pageInfo Initial pagination information. * @param loader Suspended function that loads paginated data. * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will * receive null as `it` parameter */ @Composable fun InfinityPagedComponent( pageInfo: Pagination, loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult, predefinedScope: CoroutineScope? = null, block: @Composable InfinityPagedComponentContext.(List?) -> Unit ) { InfinityPagedComponent( pageInfo.page, pageInfo.size, loader, predefinedScope, block ) } /** * Overloaded composable function for an infinitely paged component. * * @param T The type of the paginated data. * @param size Number of items per page. * @param loader Suspended function that loads paginated data. * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will * receive null as `it` parameter */ @Composable fun InfinityPagedComponent( size: Int, loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult, predefinedScope: CoroutineScope? = null, block: @Composable InfinityPagedComponentContext.(List?) -> Unit ) { InfinityPagedComponent(0, size, loader, predefinedScope, block) }