UI State with Sealed Classes

Modeling UI State with Sealed Classes

A catalog screen modeled as data class(val isLoading: Boolean, val books: List<Book>?, val error: String?) allows impossible combinations, such as loading and failed. A sealed type allows exactly three states, each carrying only the data valid in it. BookNest's catalog uses this generic LoadState.

BookNest's LoadState and a render function over it (LoadState.kt)Kotlin
sealed interface LoadState<out T> {
  data object Loading : LoadState<Nothing>
  data class Success<T>(val data: T) : LoadState<T>
  data class Error(val message: String) : LoadState<Nothing>
}
fun render(state: LoadState<List<String>>): String = when (state) {
  LoadState.Loading -> "[spinner]"
  is LoadState.Success if state.data.isEmpty() -> "No books yet"
  is LoadState.Success -> state.data.joinToString()
  is LoadState.Error -> "${state.message} [Retry]"
}
fun main() {
  listOf(LoadState.Loading, LoadState.Error("No connection"), LoadState.Success(emptyList()),
    LoadState.Success(listOf("The Quiet Harbor", "Salt and Saffron")))
    .forEach { println(render(it)) }
}
Output
[spinner]
No connection [Retry]
No books yet
The Quiet Harbor, Salt and Saffron

Loading and Error hold no book data, so they are LoadState<Nothing>; the out modifier (Variance) makes that a subtype of every LoadState<T>, so one Loading object serves every screen. In ViewModel and UDF a ViewModel exposes StateFlow<LoadState<List<Book>>>, and the composable runs this same when to draw a progress indicator, the book list or an error with Retry.