iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…