icerockdev / moko-paging

Pagination logic in common code for mobile (android & ios) Kotlin Multiplatform development
https://moko.icerock.dev/
Apache License 2.0
59 stars 7 forks source link

State machine for pagination logic #27

Open Alex009 opened 3 years ago

Alex009 commented 3 years ago

Here some research with states without boolean livedatas:

sealed class NewsItem {

    data class Short(
        override val id: Int,
        override val title: String
    ) : NewsItem()

    data class Detailed(
        override val id: Int,
        override val title: String,
        val text: String
    ) : NewsItem()

    abstract val id: Int
    abstract val title: String
}

interface NewsApi {
    suspend fun loadPage(page: Int, size: Int): List<NewsItem.Short>
    suspend fun loadDetails(id: Int): NewsItem.Detailed
}

class NewsRepository(
    private val api: NewsApi
) {
    private val _newsState = MutableStateFlow<ResourceStateThrow<PagingDataState<NewsItem>>>(
        value = ResourceState.Empty()
    )
    val newsState: StateFlow<ResourceStateThrow<PagingDataState<NewsItem>>> get() = _newsState

    suspend fun loadFirstPage() {
        val state = _newsState.value
        if (state !is ResourceState.Empty && state !is ResourceState.Error) return

        _newsState.value = ResourceState.Loading()
        _newsState.value = try {
            val firstPage = api.loadPage(0, PAGE_SIZE)
            val dataState = PagingDataState.Normal<NewsItem>(firstPage)
            ResourceState.Data(data = dataState)
        } catch (exc: Exception) {
            ResourceState.Error(exc)
        }
    }

    suspend fun loadNextPage() {
        val state = _newsState.value
        if (state !is ResourceState.Data) return
        if (state.data !is PagingDataState.Normal) return

        val currentItems = state.data.items
        val loadingState = PagingDataState.LoadNextPage(currentItems)
        _newsState.value = ResourceState.Data(loadingState)

        val nextPageIndex = (currentItems.size / PAGE_SIZE) + 1
        val nextPageItems = api.loadPage(nextPageIndex, PAGE_SIZE)

        val itemsToAdd = nextPageItems.filter { currentItems.contains(it).not() }

        val newState = PagingDataState.Normal(currentItems + itemsToAdd)
        _newsState.value = ResourceState.Data(newState)
    }

    suspend fun refreshData() {
        val state = _newsState.value
        if (state !is ResourceState.Data) return
        if (state.data !is PagingDataState.Normal) return

        val currentItems = state.data.items
        val loadingState = PagingDataState.Refresh(currentItems)
        _newsState.value = ResourceState.Data(loadingState)

        val updatedFirstPage = api.loadPage(0, PAGE_SIZE)

        val newState = PagingDataState.Normal<NewsItem>(updatedFirstPage)
        _newsState.value = ResourceState.Data(newState)
    }

    suspend fun loadDetails(id: Int): NewsItem.Detailed {
        val detailed = api.loadDetails(id)

        val currentState = _newsState.value
        if (currentState is ResourceState.Data && currentState.data is PagingDataState.Normal) {
            val items = currentState.data.items
            val updatedNews = items.map {
                if (it.id == id) detailed
                else it
            }
            _newsState.value = ResourceState.Data(PagingDataState.Normal(updatedNews))
        } else {
            throw IllegalStateException("try to update details item while in not normal data state")
        }

        return detailed
    }

    private companion object {
        const val PAGE_SIZE = 10
    }
}

typealias ResourceStateThrow<T> = ResourceState<T, Throwable>

sealed class ResourceState<T, E> {
    class Empty<T, E> : ResourceState<T, E>()
    class Loading<T, E> : ResourceState<T, E>()
    data class Data<T, E>(val data: T) : ResourceState<T, E>()
    data class Error<T, E>(val error: E) : ResourceState<T, E>()
}

sealed class PagingDataState<T> {
    data class Normal<T>(override val items: List<T>) : PagingDataState<T>()
    data class Refresh<T>(override val items: List<T>) : PagingDataState<T>()
    data class LoadNextPage<T>(override val items: List<T>) : PagingDataState<T>()

    abstract val items: List<T>
}