sealed class
A sealed class restricts inheritance to a known set of subtypes — usually data classes for variants. Pair with when for exhaustive pattern matching at compile time.
Sealed class, sealed interface, exhaustive when
EXAMPLE
// 1) Define a sealed hierarchy
sealed class Result<out T> {
data class Ok<T>(val value: T) : Result<T>()
data class Err(val message: String): Result<Nothing>()
object Loading : Result<Nothing>()
}
// 2) Use it — when is EXHAUSTIVE
fun render(r: Result<String>): String = when (r) {
is Result.Ok -> "Got \${r.value}"
is Result.Err -> "Failed: \${r.message}"
Result.Loading -> "Loading…"
}
// Compiler enforces every variant — add a case, add a branch.
// 3) Smart casts
fun show(r: Result<String>) {
when (r) {
is Result.Ok -> println(r.value.length) // r.value: String here
is Result.Err -> println(r.message.uppercase())
Result.Loading -> println("...")
}
}
// 4) Sealed interface — multiple inheritance / cross-cutting
sealed interface UiState {
object Loading : UiState
data class Success(val data: List<Item>) : UiState
data class Failure(val msg: String, val canRetry: Boolean) : UiState
}
fun reduce(state: UiState, action: Action): UiState = when {
state is UiState.Loading && action is Loaded -> UiState.Success(action.data)
state is UiState.Loading && action is Failed -> UiState.Failure(action.msg, canRetry = true)
state is UiState.Failure && action is Retry -> UiState.Loading
else -> state
}
// 5) Domain events
sealed class OrderEvent {
data class Created(val orderId: Long, val total: Double) : OrderEvent()
data class Paid(val orderId: Long) : OrderEvent()
data class Shipped(val orderId: Long, val trackingNo: String): OrderEvent()
data class Cancelled(val orderId: Long, val reason: String) : OrderEvent()
}
fun handle(e: OrderEvent) = when (e) {
is OrderEvent.Created -> notifyCreated(e.orderId)
is OrderEvent.Paid -> queueFulfilment(e.orderId)
is OrderEvent.Shipped -> emailTrackingNo(e.orderId, e.trackingNo)
is OrderEvent.Cancelled -> refund(e.orderId, e.reason)
}
// 6) Sealed + data class + recursive types — JSON-ish trees
sealed class JsonNode
object JsonNull : JsonNode()
data class JsonBool (val v: Boolean) : JsonNode()
data class JsonNumber (val v: Double) : JsonNode()
data class JsonString (val v: String) : JsonNode()
data class JsonArray (val v: List<JsonNode>) : JsonNode()
data class JsonObject (val v: Map<String, JsonNode>) : JsonNode()
fun toString(n: JsonNode): String = when (n) {
JsonNull -> "null"
is JsonBool -> n.v.toString()
is JsonNumber -> n.v.toString()
is JsonString -> "\"\${n.v}\""
is JsonArray -> n.v.joinToString(prefix = "[", postfix = "]") { toString(it) }
is JsonObject -> n.v.entries.joinToString(prefix = "{", postfix = "}") { (k, v) -> "\"\$k\":\${toString(v)}" }
}
// 7) Exhaustive expression — when used AS an expression, Kotlin enforces exhaustiveness
fun message(state: UiState) = when (state) {
UiState.Loading -> "…"
is UiState.Success -> "Got \${state.data.size}"
is UiState.Failure -> state.msg
// missing arm → compile error
}
// 8) Sealed vs enum vs interface
// Sealed class : multiple variants, EACH CAN HAVE DIFFERENT DATA shape. Use for discriminated unions.
// Enum : finite set of CONSTANTS, all share the same shape. Use for stable identifiers.
// Interface : open contract; anyone in any module can implement. Use for plugin points.
// 9) Restrictions
// • All direct subclasses must be in the SAME PACKAGE + module
// • Sealed class subclasses must be top-level OR nested in the sealed class itself
// • Sealed interface subclasses can be anywhere in the same module
// 10) Real-world Android pattern — Loadable state
// In a ViewModel:
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun load() = viewModelScope.launch {
_state.value = UiState.Loading
_state.value = runCatching { repo.fetch() }
.fold({ UiState.Success(it) }, { UiState.Failure(it.message ?: "", canRetry = true) })
}
// In Compose:
val state by viewModel.state.collectAsState()
when (val s = state) {
UiState.Loading -> LoadingIndicator()
is UiState.Success -> ItemList(s.data)
is UiState.Failure -> ErrorView(s.msg, retry = s.canRetry)
}
Why it matters
Sealed classes give you Rust enums + TypeScript discriminated unions in Kotlin. The killer feature: exhaustive when — add a variant, compiler flags every site that needs updating. Domain models stay honest.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
sealed class Shape
data class Circle(val r: Double) : Shape()
data class Rect(val w: Double, val h: Double) : Shape()
fun area(s: Shape) = when (s) {
is Circle -> Math.PI * s.r * s.r
is Rect -> s.w * s.h
}
Try it Yourself »
Discussion
Loading…