Skip to main content

Android application: result to UI state

sealed interface UserResult {
data object Loading : UserResult
data class Success(val name: String, val unread: Int) : UserResult
data class Error(val cause: Throwable) : UserResult
}

data class UserUiState(
val title: String,
val showProgress: Boolean,
val errorMessage: String? = null,
)

fun UserResult.toUiState(): UserUiState = when (this) {
UserResult.Loading -> UserUiState("Loading", true)
is UserResult.Success -> UserUiState(
title = if (unread > 0) "$name ($unread)" else name,
showProgress = false,
)
is UserResult.Error -> UserUiState(
title = "Could not load user",
showProgress = false,
errorMessage = cause.message ?: "Unknown error",
)
}

What is happening here:

  1. UserResult lists every result the repository can produce.
  2. when handles every result; no state is silently ignored.
  3. Inside Success and Error, Kotlin smart-casts this, so their properties are available safely.
  4. Each branch creates one complete UserUiState instead of changing several variables one by one.
  5. errorMessage is nullable because “there is no error” is a valid state.

Common mistakes​

MistakeProblemBetter choice
var everywhereToo many possible statesStart with val
Treating val as immutableObject may still mutateImmutable models/APIs
Comparing content using ===Tests identityUsually use ==
0..list.sizeInvalid final indexlist.indices
else on every sealed whenHides new variantsExhaust every case
Forced as on inputWrong input crashesas?, validate, model
Re-reading mutable propertiesCheck may become staleCapture local val

Why this matters in a larger Android app​

  • A sealed state prevents impossible combinations such as isLoading = true and hasError = true unless that combination is intentionally modeled.
  • Immutable values are easier to pass between ViewModels, repositories, coroutines, and Compose without hidden changes.
  • Convert untrusted strings, numbers, and Any? values when they enter the feature, so inner layers receive reliable types.
  • Decide what equality means because Compose, lists, flows, caches, and tests use it.
  • Use exhaustive when expressions so a new state creates a compiler reminder wherever handling is missing.

Interview questions​

What is the difference between val and immutability?

val prevents reference reassignment; it does not prevent mutation of the referenced object.

Why is if an expression?

It can produce a value. When used as a value, its branches must produce compatible result types.

What is the difference between == and ===?

== performs null-safe structural equality; === checks reference identity.

When does a smart cast fail?

When the compiler cannot prove the value remains stable between the check and use.

Why omit else for sealed state?

A new variant then exposes every consumer requiring a decision through compilation errors.

Does Kotlin prevent integer overflow?

No. Standard JVM integer arithmetic can overflow and wrap.

Exercises​

  1. Model Idle, Loading, Content, Empty, and Failure, then map every state to a title.
  2. Predict == and === for two equal data-class instances.
  3. Replace temporary mutable variables with expression-based initialization.
  4. Safely cast, trim, and validate an Any? route argument without throwing.
  5. Explain why mutating a field involved in hashCode can lose a map key.

Chapter checklist​

  • I distinguish a read-only reference from an immutable object.
  • I know when explicit types improve an API.
  • I use if, when, and blocks as expressions.
  • I choose safe loop boundaries.
  • I distinguish equality, identity, unsafe casts, and safe casts.
  • I can explain smart-cast stability.
  • I can model exhaustive immutable Android UI state.