Skip to content

State machine for pagination logic #27

Description

@Alex009

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>
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions