All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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