Sealed Class is a special type of class in Kotlin that restricts the inheritance hierarchy to a fixed set of subtypes. All subclasses are declared in the same file and are known to the compiler, which allows using an exhaustive when block without a mandatory else branch. According to Kotlin Docs, 2026, sealed classes are a key mechanism for representing restricted hierarchies such as states, error types, and UI events.
Key Takeaways
Sealed Class is a class in Kotlin marked with the sealed modifier. It defines a restricted type hierarchy: all possible subclasses are listed in the same file, and the compiler knows about each of them. This distinguishes a sealed class from a regular open class whose subclasses can be declared anywhere.
The main purpose of a sealed class is type-safe representation of a finite set of variants. Each subclass can have its own data structure, making sealed class more flexible than enum. At runtime, a sealed class is a regular abstract class; the compiler only imposes restrictions at compile time.
Sealed class is especially useful in Android app architecture: UI states, network request results, navigation events similar to Intents, and of course error hierarchies are typical use cases.
At compile time, sealed class is optimized into a jump table for when expressions, making it more performant than chains of if-else. Combined with data class, each subclass can contain not only state but also methods, allowing self-documenting domain models without boilerplate code.
Sealed class is also effective for representing state machines in mobile applications. Each state is a separate subclass with unique parameters, and transitions between states are controlled via when expressions. The compiler guarantees that all possible states are handled, eliminating runtime errors when changing UI state or business logic.
Beginning Kotlin developers often confuse sealed class with enum, since both restrict the set of values. However, there is a fundamental difference: enum is a set of constants of the same type, while sealed class is a hierarchy of different types.
Enum is optimal when all variants are constants without additional structure. For example, days of the week, order statuses, or action types without parameters. Each enum value is a singleton with a fixed name.
Sealed class is needed when each variant has its own data. For example, a network error contains a response code, a parsing error contains details, and an authorization error contains a message. Each sealed class subclass is a separate type with unique fields.
// Enum — all variants of one type
enum class Status { LOADING, SUCCESS, ERROR }
// Sealed class — each variant with its own data
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
Since Kotlin 1.5, it is possible to declare sealed interface. This extends the sealed concept to interfaces: a sealed interface also has a fixed set of implementations but supports multiple inheritance.
Sealed interface is convenient when subclasses need to implement multiple contracts simultaneously. For example, a UI event can be both clickable and trackable. With sealed class, you would have to choose one base class; with sealed interface, the subclass implements both.
Sealed class is a class, so each subclass can have only one parent. Sealed interface solves this problem but cannot hold state. The choice between them depends on the task: if shared logic with fields is needed — use sealed class; if contract flexibility is needed — use sealed interface.
sealed interface ScreenEvent {
data class Refresh(val force: Boolean) : ScreenEvent
data class Navigate(val route: String) : ScreenEvent
data class ShowError(val toast: String) : ScreenEvent
}
sealed interface AnalyticsEvent {
val name: String
val params: Map<String, Any>
}
// The subclass implements both interfaces
data class LoginClicked(
override val name: String = "login_click",
override val params: Map<String, Any> = emptyMap()
) : ScreenEvent, AnalyticsEvent
One of the main uses of sealed class in mobile development is type-safe error hierarchies. Instead of throwing exceptions of different types or using a general Exception, sealed class collects all possible domain errors into a single type.
Create a sealed class DomainError and list all failure types as subclasses. Each subclass contains only the data that is relevant for that particular error type. The compiler guarantees that when handling the error, you will not forget any variant.
Consider an app with authorization where different failure scenarios are possible: wrong password, account blocked, server issue. Sealed class combines them into a single type with exhaustive handling.
sealed class AuthError {
data class InvalidCredentials(
val attempts: Int
) : AuthError()
data class AccountBlocked(
val until: Long
) : AuthError()
data class NetworkFailure(
val cause: Throwable
) : AuthError()
object ServerError : AuthError()
}
fun handleError(error: AuthError): String = when (error) {
is AuthError.InvalidCredentials ->
"Remaining attempts: ${3 - error.attempts}"
is AuthError.AccountBlocked ->
"Access blocked until ${Date(error.until)}"
is AuthError.NetworkFailure ->
"Check connection: ${error.cause.localizedMessage}"
AuthError.ServerError ->
"Server temporarily unavailable"
}
Sealed class has become a standard tool in Android application architecture. Let us look at three key patterns where sealed class is indispensable in mobile development.
It is also worth noting the use of sealed class in Clean Architecture. Each layer (data, domain, presentation) uses sealed class for its error types, and mappers convert one sealed class to another. For example, DataError from the data layer is mapped to DomainError for business logic, and then to UiState for the presentation layer. This preserves type safety at all levels of the application and guarantees that no error remains unhandled.
Testing sealed class requires a special approach since each subclass is a separate type with its own state. It is recommended to write parameterized tests that iterate over all sealed class subclasses. This guarantees that when expressions cover all variants, including new ones added when extending the hierarchy.
For UI tests, sealed class as UiState allows checking the display of each state: Loading shows a spinner, Content shows data, Error shows an error message. Since sealed class is finite, test coverage of all states provides complete confidence in the correctness of UI logic.
Despite the simplicity of the concept, developers regularly make mistakes when designing sealed class hierarchies. Let us look at the main problems and how to avoid them.
Frequently Asked Questions
Yes, a sealed class can contain abstract methods, and each subclass is required to implement them. This is convenient when all variants should provide a common interface but with different execution logic.
In Java 17+, sealed classes and interfaces with the sealed modifier were introduced. Android currently supports Java 17 partially, but in Kotlin projects, sealed class has been available since Kotlin 1.0 without restrictions.
Yes, one sealed class can be a subclass of another. The sealed class hierarchy remains finite: the compiler knows all subclasses at every level. This allows building detailed error classifications.
Sealed class does not create runtime overhead. The compiler optimizes when expressions with sealed classes into jump tables (tableswitch), which is faster than chains of if-else. Performance is identical to enum.
Each sealed class subclass is tested separately. Since sealed class is finite, you can write a parameterized test that iterates over all variants. This provides full coverage of when-block branches.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also