Sealed Classes
Restricting a type to a known, closed set of subclasses so the compiler can verify every case is handled.
3 phút đọc
A sealed class restricts which classes are allowed to extend it: every subclass must be declared in the same file (or, since Kotlin 1.5, the same compilation module). That restriction sounds limiting, but it's exactly what unlocks one of Kotlin's most useful safety checks — an exhaustive when.
Declaring one
A common use is modeling a result that can succeed or fail, each carrying different data:
sealed class NetworkResult
data class Success(val data: String) : NetworkResult()
data class Error(val message: String, val code: Int) : NetworkResult()
object Loading : NetworkResult()Success and Error are data classes carrying different payloads, and Loading is a plain object (covered more in the next section) since it doesn't need to hold any data — there's only ever one Loading state.
Exhaustive when — without an else
Because the compiler knows the complete list of NetworkResult subclasses — nothing outside this file can add another one — a when over a sealed class can be exhaustive without an else branch at all:
fun handleResult(result: NetworkResult) {
val message = when (result) {
is Success -> "Got data: ${result.data}"
is Error -> "Failed (${result.code}): ${result.message}"
Loading -> "Still loading..."
}
println(message)
}Leave out one of the three branches, and this becomes a compile error, not a bug you discover later:
// Compile error: 'when' expression must be exhaustive,
// add necessary 'is Error' branch or 'else' branch
val message = when (result) {
is Success -> "Got data: ${result.data}"
Loading -> "Still loading..."
}Why this beats an enum or a plain class hierarchy
An enum can represent a fixed set of cases, but each case can't carry different data shapes — every entry has the same properties. A regular open class can carry different data per subclass, but anyone, anywhere, can add a new subclass the compiler doesn't know about, so it can never guarantee a when over it is exhaustive. Sealed classes combine both strengths: different data per case, plus a closed, compiler-verified set of cases.
The real payoff shows up when you add a new case later. Add a data class TimedOut(val seconds: Int) : NetworkResult() to the hierarchy above, and every existing when block that handles NetworkResult without an else branch immediately fails to compile — right at the spot that needs updating, across the whole codebase — instead of silently ignoring the new case at runtime.
sealed interfaces
Kotlin also supports sealed interface, useful when the shared contract matters more than shared implementation, and a class needs to belong to more than one closed hierarchy:
sealed interface UiState
object Idle : UiState
data class Content(val text: String) : UiState
data class Failed(val reason: String) : UiStateSealed classes are a common building block for state that flows through an app — a screen's UI state, a parsed result, a step in a workflow. The next lesson shifts to a different kind of problem entirely: running work without blocking, using Kotlin's coroutines.