
me.aartikov.sesame.loading.paged.PagedLoading.kt Maven / Gradle / Ivy
package me.aartikov.sesame.loading.paged
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import me.aartikov.sesame.loading.paged.internal.PagedLoadingImpl
/**
* Data loader for [PagedLoading].
*/
interface PagedLoader {
/**
* Loads a first page.
* @param fresh indicates that fresh data is required.
* @return page with data.
*/
suspend fun loadFirstPage(fresh: Boolean): Page
/**
* Loads the next page.
* @param pagingInfo information about the already loaded pages. See: [PagingInfo].
* @return data for the next page. Empty list means that the end of data is reached.
*/
suspend fun loadNextPage(pagingInfo: PagingInfo): Page
}
/**
* Helps to load paged data and manage loading state.
*/
interface PagedLoading {
/**
* Loading state.
*/
sealed class State {
/**
* Empty list is loaded or loading is not started yet.
*/
object Empty : State()
/**
* Loading is in progress and there is no previously loaded data.
*/
object Loading : State()
/**
* Loading error has occurred and there is no previously loaded data.
*/
data class Error(val throwable: Throwable) : State()
/**
* Non-empty list has been loaded.
* @property pageCount count of loaded pages.
* @property data not empty list, sequentially merged data of the all loaded pages.
* @property status see: [DataStatus].
*/
data class Data(
val pageCount: Int,
val data: List,
val status: DataStatus = DataStatus.Normal
) : State() {
val loadMoreEnabled: Boolean get() = status == DataStatus.Normal
val refreshing: Boolean get() = status == DataStatus.Refreshing
val loadingMore: Boolean get() = status == DataStatus.LoadingMore
val fullData: Boolean get() = status == DataStatus.FullData
}
}
enum class DataStatus {
/**
* Just a data, there is no in progress loading, the end of a list is not reached.
*/
Normal,
/**
* First page reloading is in progress.
*/
Refreshing,
/**
* Loading of a next page is in progress.
*/
LoadingMore,
/**
* The end of a list is reached.
*/
FullData
}
/**
* Loading event.
*/
sealed class Event {
/**
* An error occurred. [stateDuringLoading] a state that was during the failed loading.
*/
data class Error(val throwable: Throwable, val stateDuringLoading: State) : Event() {
/**
* Is true when there is previously loaded data. It is useful to not show an error dialog when a fullscreen error is already shown.
*/
val hasData get() = stateDuringLoading is State.Data
}
}
/**
* Flow of loading states.
*/
val stateFlow: StateFlow>
/**
* Flow of loading events.
*/
val eventFlow: Flow>
/**
* Requests to load a first page.
* @param fresh indicates that fresh data is required. See [PagedLoader.loadFirstPage].
* @param reset if true than previously loaded data will be instantly dropped and in progress loading will be canceled.
* Otherwise previously loaded data will be preserved until successful outcome, if another loadFirstPage request is in progress
* than new one will be ignored, if loadMore request is in progress than it will be canceled.
*/
fun loadFirstPage(fresh: Boolean, reset: Boolean = false)
/**
* Requests to load the next page. Loaded data will be added to the end of a previously loaded list.
* The request will be ignored if another one (loadMore or loadFirstPage) is already in progress.
*/
fun loadMore()
/**
* Requests to cancel in progress loading.
* @param reset if true than state will be reset to [PagedLoading.State.Empty].
*/
fun cancel(reset: Boolean = false)
/**
* Mutates [PagedLoading.State.Data.data] with a [transform] function.
*/
fun mutateData(transform: (List) -> List)
}
/**
* A shortcut for loadFirstPage(fresh = true, reset = false). Requests to load a fresh first page and preserve the old data until successful outcome.
*/
fun PagedLoading.refresh() = loadFirstPage(fresh = true, reset = false)
/**
* A shortcut for loadFirstPage(fresh, reset = true). Requests to drop old data and load a first page.
* @param fresh indicates that fresh data is required. See [PagedLoader.loadFirstPage].
*/
fun PagedLoading.restart(fresh: Boolean = true) = loadFirstPage(fresh, reset = true)
/**
* A shortcut for cancel(reset = true). Cancels loading and sets state to [PagedLoading.State.Empty].
*/
fun PagedLoading.reset() = cancel(reset = true)
/**
* Returns current [PagedLoading.State].
*/
val PagedLoading.state: PagedLoading.State get() = stateFlow.value
/**
* Returns [PagedLoading.State.Data.data] if it is available or null otherwise
*/
val PagedLoading.dataOrNull: List? get() = state.dataOrNull
/**
* Returns [PagedLoading.State.Error.throwable] if it is available or null otherwise
*/
val PagedLoading<*>.errorOrNull: Throwable? get() = state.errorOrNull
/**
* Returns [PagedLoading.State.Data.data] if it is available or null otherwise
*/
val PagedLoading.State.dataOrNull: List? get() = (this as? PagedLoading.State.Data)?.data
/**
* Returns [PagedLoading.State.Error.throwable] if it is available or null otherwise
*/
val PagedLoading.State<*>.errorOrNull: Throwable? get() = (this as? PagedLoading.State.Error)?.throwable
/**
* A helper method to handle [PagedLoading.Event.Error].
*/
fun PagedLoading.handleErrors(
scope: CoroutineScope,
handler: (PagedLoading.Event.Error) -> Unit
): Job {
return eventFlow.filterIsInstance>()
.onEach {
handler(it)
}
.launchIn(scope)
}
/**
* Creates an implementation of [PagedLoading].
*/
fun PagedLoading(
scope: CoroutineScope,
loader: PagedLoader,
initialState: PagedLoading.State = PagedLoading.State.Empty,
dataMerger: DataMerger = SimpleDataMerger()
): PagedLoading {
return PagedLoadingImpl(scope, loader, initialState, dataMerger)
}
/**
* Creates an implementation of [PagedLoading].
*/
fun PagedLoading(
scope: CoroutineScope,
loadFirstPage: suspend (fresh: Boolean) -> Page,
loadNextPage: suspend (pagingInfo: PagingInfo) -> Page,
initialState: PagedLoading.State = PagedLoading.State.Empty,
dataMerger: DataMerger = SimpleDataMerger()
): PagedLoading {
val loader = object : PagedLoader {
override suspend fun loadFirstPage(fresh: Boolean): Page = loadFirstPage(fresh)
override suspend fun loadNextPage(pagingInfo: PagingInfo): Page = loadNextPage(pagingInfo)
}
return PagedLoading(scope, loader, initialState, dataMerger)
}
/**
* Creates an implementation of [PagedLoading].
*/
fun PagedLoading(
scope: CoroutineScope,
loadPage: suspend (pagingInfo: PagingInfo) -> Page,
initialState: PagedLoading.State = PagedLoading.State.Empty,
dataMerger: DataMerger = SimpleDataMerger()
): PagedLoading {
val loader = object : PagedLoader {
override suspend fun loadFirstPage(fresh: Boolean): Page =
loadPage(PagingInfo(0, emptyList()))
override suspend fun loadNextPage(pagingInfo: PagingInfo): Page = loadPage(pagingInfo)
}
return PagedLoading(scope, loader, initialState, dataMerger)
}
/**
* Returns new [PagedLoading.State] of applying the given [transform] function to original [PagedLoading.State.Data.data].
*/
fun PagedLoading.State.mapData(transform: (List) -> List): PagedLoading.State {
return when (this) {
PagedLoading.State.Empty -> PagedLoading.State.Empty
PagedLoading.State.Loading -> PagedLoading.State.Loading
is PagedLoading.State.Error -> PagedLoading.State.Error(throwable)
is PagedLoading.State.Data -> PagedLoading.State.Data(pageCount, transform(data), status)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy