moko-paging
moko-paging copied to clipboard
State machine for pagination logic
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>
}