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:
UserResultlists every result the repository can produce.whenhandles every result; no state is silently ignored.- Inside
SuccessandError, Kotlin smart-caststhis, so their properties are available safely. - Each branch creates one complete
UserUiStateinstead of changing several variables one by one. errorMessageis nullable because “there is no error” is a valid state.
Common mistakes
| Mistake | Problem | Better choice |
|---|---|---|
var everywhere | Too many possible states | Start with val |
Treating val as immutable | Object may still mutate | Immutable models/APIs |
Comparing content using === | Tests identity | Usually use == |
0..list.size | Invalid final index | list.indices |
else on every sealed when | Hides new variants | Exhaust every case |
Forced as on input | Wrong input crashes | as?, validate, model |
| Re-reading mutable properties | Check may become stale | Capture local val |
Why this matters in a larger Android app
- A sealed state prevents impossible combinations such as
isLoading = trueandhasError = trueunless 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
whenexpressions 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
- Model
Idle,Loading,Content,Empty, andFailure, then map every state to a title. - Predict
==and===for two equal data-class instances. - Replace temporary mutable variables with expression-based initialization.
- Safely cast, trim, and validate an
Any?route argument without throwing. - Explain why mutating a field involved in
hashCodecan 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.