commonMain.fr.haan.bipak.Pager.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bipak-core-jvm Show documentation
Show all versions of bipak-core-jvm Show documentation
Core library of BiPaK is a Kotlin multiplatform paging library.
package fr.haan.bipak
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
/**
* Primary entry point into Paging; constructor for a reactive stream of [PagingData].
*
* Each [PagingData] represents the current paging state and data.
* [Pager] will fetch relevant data based on [PagingViewEvent] received via [subscribeToViewEvents]
*
*/
public class Pager(
/**
* The [CoroutineScope] used to fetch the content asynchronously
*/
private val scope: CoroutineScope,
/**
* The [PagingDataSource] from which data will be fetched
*/
private val source: PagingDataSource,
/**
* This is the first page identifier that has to be fetched
*/
private val initialKey: Key,
/**
* Optional paging configuration. @see [PagingConfig]
*/
private val config: PagingConfig = PagingConfig(),
) {
private val _dataFlow: MutableStateFlow> = MutableStateFlow(PagingData.empty())
/**
* Hot [Flow] of [PagingData], exposing the current state & data.
* A new item will be emitted whenever the [PagingData.LoadState] or [PagingData.list] is updated
*/
public val dataFlow: Flow> = _dataFlow
private sealed class InternalPage {
class ToFetch : InternalPage()
class Loading(internal val result: Deferred>) :
InternalPage()
data class Page(
val data: List,
val nextKey: Key?,
val totalCount: Int?,
) : InternalPage()
data class Error(
val error: Throwable,
) : InternalPage()
}
private val currentPagingData = ArrayList>()
/**
* Collect events from the view to triggers page loads.
* Will suspend until eventFlow is terminated by sending [PagingViewEvent.Terminate].
* This can be done by calling [PagingEventEmitter.stop]
* @param eventFlow The flow of event coming from the view; May be emitted by [PagingEventEmitter]
*
*/
public suspend fun subscribeToViewEvents(eventFlow: MutableSharedFlow) {
eventFlow
.onEach { viewEvent ->
pagingDebugLog("Pager: received: ${viewEvent::class.simpleName}")
if (viewEvent is PagingViewEvent.Terminate) {
// This prevents terminating again when re-subscribing
eventFlow.resetReplayCache()
}
}
// Allows to terminate the Flow
.takeWhile { it !is PagingViewEvent.Terminate }
.collect { viewEvent ->
when (viewEvent) {
is PagingViewEvent.ElementRequested -> {
pagingDebugLog("ElementRequested: ${viewEvent.index}")
fetchPageAtIndex(getPageIndexForElementIndex(viewEvent))
}
PagingViewEvent.Idle -> {
/* Default state, do nothing */
}
PagingViewEvent.Retry -> fetchPageAtIndex(getPageIndexToRetry(), retry = true)
PagingViewEvent.Terminate -> return@collect // We shouldn't go there, thanks to takeWhile operator
}
}
}
private fun getPageIndexToRetry(): Int {
return currentPagingData.indexOfFirst { it is InternalPage.Error }
}
private fun getPageIndexForElementIndex(event: PagingViewEvent.ElementRequested): Int {
return (event.index + config.prefetchDistance) / config.pageSize
}
private suspend fun fetchPageAtIndex(pageIndex: Int, retry: Boolean = false) {
pagingDebugLog("fetchPageAtIndex(): $pageIndex")
if (pageIndex < 0) return
if (currentPagingData.getOrNull(pageIndex) is InternalPage.Page) return
var currentIndex = 0
var previousResult: InternalPage.Page? = null
while (true) {
val currentPage: InternalPage =
(currentPagingData.getOrNull(currentIndex) ?: InternalPage.ToFetch())
pagingDebugLog("fetchPageAtIndex() currentIndex: $currentIndex, currentPage: $currentPage")
val shouldRetry = retry && currentPage is InternalPage.Error
pagingDebugLog("shouldRetry: $shouldRetry")
when {
currentPage is InternalPage.ToFetch || shouldRetry -> {
val deferredResult = fetchPageAtKey(currentIndex, previousResult?.nextKey ?: initialKey)
// Emit load state
val loadState = InternalPage.Loading(deferredResult)
currentPagingData.addOrReplace(currentIndex, loadState)
handleResult(loadState)
val result = deferredResult.await()
pagingDebugLog("fetchPageAtIndex(): result $result")
when (result) {
is InternalPage.Error -> {
currentPagingData.addOrReplace(currentIndex, result)
handleResult(result)
return
}
is InternalPage.Page -> {
currentPagingData.addOrReplace(currentIndex, result)
if (currentIndex == pageIndex) {
handleResult(result)
return
} else {
previousResult = result
if (result.nextKey != null) {
currentIndex++
currentPagingData.addOrReplace(currentIndex, InternalPage.ToFetch())
handleResult(result)
} else {
// End of list
return
}
}
}
is InternalPage.Loading -> assert(false) { "Shoudln't be in loading state after await" }
is InternalPage.ToFetch -> assert(false) { "Shoudln't be in ToFetch state after await" }
}
}
currentPage is InternalPage.Error -> {
// Do nothing, retry has to be called explicitly
return
}
currentPage is InternalPage.Loading -> {
// Do nothing, result will be emitted on [dataFlow]
return
}
currentPage is InternalPage.Page -> {
// Page requested is already in cache
if (currentIndex == pageIndex) {
// We have everything asked, exit
return
} else {
// Try to load next page
if (currentPage.nextKey != null) {
previousResult = currentPage
currentIndex++
} else {
// End of list
return
}
}
}
}
}
}
private fun assert(value: Boolean, lazyMessage: () -> String) {
if(!value) throw IllegalStateException(lazyMessage())
}
private suspend fun fetchPageAtKey(pageIndex: Int, key: Key): Deferred> {
pagingDebugLog("fetchPageAtKey(): pageIndex: $pageIndex, key: $key")
val deferredResult: Deferred> = scope.async {
val result = source.load(
PagingDataSource.LoadParams.Append(
key,
config.pageSize,
)
)
return@async when (result) {
is PagingDataSource.LoadResult.Error -> {
InternalPage.Error(
result.throwable
)
}
is PagingDataSource.LoadResult.Page -> {
InternalPage.Page(
result.data,
nextKey = result.nextKey,
totalCount = result.totalCount,
)
}
}
}
return deferredResult
}
private fun handleResult(result: InternalPage) {
when (result) {
is InternalPage.Error -> handleError(result.error)
is InternalPage.Page, is InternalPage.Loading -> handleSourceResult()
is InternalPage.ToFetch -> { /* NOOP */
}
}
}
private fun getCurrentInternaList(): List> {
return currentPagingData
.takeWhile { it is InternalPage.Page }
.map { it as InternalPage.Page }
}
private fun getCurrentList(): List {
val currentList = getCurrentInternaList()
.map { it.data }
.fold(emptyList()) { acc, next -> acc + next }
return currentList
}
private fun getCurrentState(): PagingData.LoadState {
val loadingPages = currentPagingData
.filter { it is InternalPage.Loading || it is InternalPage.ToFetch }
if (loadingPages.isNotEmpty()) {
return PagingData.LoadState.Loading
} else {
return PagingData.LoadState.NotLoading
}
}
private fun handleSourceResult() {
_dataFlow.tryEmit(
PagingData(
list = getCurrentList(),
state = getCurrentState(),
totalCount = getCurrentInternaList().lastOrNull()?.totalCount,
)
)
}
private fun handleError(error: Throwable) {
_dataFlow.tryEmit(
PagingData(
list = getCurrentList(),
state = PagingData.LoadState.Error(error),
totalCount = getCurrentInternaList().lastOrNull()?.totalCount,
)
)
}
private fun ArrayList.addOrReplace(index: Int, element: E) {
if (this.lastIndex < index) {
this.add(index, element)
} else {
this.set(index, element)
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy