0Pricing
Kotlin Academy · Lesson

Modeling UI State with Sealed Classes

Apply sealed classes to represent Loading, Success, and Error UI states.

Modeling UI State with Sealed Classes is a free Kotlin Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The UI State Problem

UI screens have multiple states: loading, showing data, showing an error, or empty. Managing these with booleans (isLoading, hasError) is fragile. Sealed classes fix this.

The Classic Boolean Hell

Using separate booleans leads to impossible states.
// Fragile: isLoading=true AND hasError=true is invalid
var isLoading = false
var hasError = false
var data: List<Item>? = null
// 8 combinations, most of which are invalid!

Sealed State: Only Valid States

A sealed class makes invalid states unrepresentable.
sealed class UiState<out T>
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String, val retryable: Boolean = true) : UiState<Nothing>()
object Empty : UiState<Nothing>()

Rendering Based on State

Use when to render the correct UI for each state.
fun render(state: UiState<List<Item>>) = when (state) {
    Loading -> showProgressBar()
    is Success -> showItems(state.data)
    is Error -> showError(state.message, state.retryable)
    Empty -> showEmptyView()
}

ViewModel Emitting States

A ViewModel emits sealed states via StateFlow.
class ItemViewModel : ViewModel() {
    private val _state = MutableStateFlow<UiState<List<Item>>>(Loading)
    val state: StateFlow<UiState<List<Item>>> = _state

    fun loadItems() = viewModelScope.launch {
        _state.value = Loading
        _state.value = try {
            Success(repository.getItems())
        } catch (e: Exception) {
            Error(e.message ?: "Unknown error")
        }
    }
}

Composing States with Content Inside

Use generic sealed states to handle any data type.
fun <T> loading(): UiState<T> = Loading
fun <T> success(data: T): UiState<T> = Success(data)
fun <T> error(msg: String): UiState<T> = Error(msg)

Pagination State Example

Sealed classes naturally model paginated list states.
sealed class PageState
object Idle : PageState()
object FirstLoad : PageState()
data class Content(val items: List<Item>, val hasMore: Boolean) : PageState()
data class LoadingMore(val items: List<Item>) : PageState()
data class Error(val items: List<Item>?, val msg: String) : PageState()

Transforming State

Map successful states without touching other cases.
fun <T, R> UiState<T>.mapData(transform: (T) -> R): UiState<R> = when (this) {
    is Success -> Success(transform(data))
    is Error -> this
    Loading -> Loading
    Empty -> Empty
}

Testing with Sealed States

States are easy to test — just check which variant was emitted.
@Test
fun testLoadingState() {
    viewModel.loadItems()
    assertEquals(Loading, viewModel.state.value)
}
@Test
fun testSuccessState() {
    viewModel.setData(listOf(item))
    assert(viewModel.state.value is Success)
}

Combined State vs Multiple Flows

A single sealed state flow is cleaner than multiple separate flows that must stay in sync.
// Fragile: two flows must stay in sync
val loading = MutableStateFlow(false)
val error = MutableStateFlow<String?>(null)

// Clean: one sealed state
val state = MutableStateFlow<UiState<List<Item>>>(Loading)

Adding Metadata to States

Sealed subtypes can carry relevant metadata.
data class Error(
    val message: String,
    val code: Int = 0,
    val retryable: Boolean = true,
    val timestamp: Long = System.currentTimeMillis()
) : UiState<Nothing>()

Quick Check

What key problem does modeling UI state with sealed classes solve?

Recap

Model UI state with sealed classes: Loading, Success(data), Error(message), Empty. Use when to render each case. One StateFlow with a sealed type beats multiple boolean flags. Next: nesting sealed hierarchies!

Frequently asked questions

Is the “Modeling UI State with Sealed Classes” lesson free?

Yes — the full text of “Modeling UI State with Sealed Classes” is free to read here on the web, and the Kotlin Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Kotlin Academy course, upgrade to CoddyKit PRO.

What will I learn in “Modeling UI State with Sealed Classes”?

Apply sealed classes to represent Loading, Success, and Error UI states. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Kotlin Academy?

No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Modeling UI State with Sealed Classes” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Kotlin Academy lesson?

Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. sealed class vs sealed interface: When to Use Each
  2. Exhaustive when with Sealed Hierarchies
  3. Modeling UI State with Sealed Classes
  4. Nesting and Combining Sealed Hierarchies
← Back to Kotlin Academy