Explanation of Sealed Classes in Kotlin
Lets get started.
Sealed classes are important feature in Kotlin that help you represent restricted class hierarchies. So all subclasses will be known at compile-time. They are particularly useful for modeling state machines, results or events where you want to ensure type safety in when expressions in Kotlin.
What Are Sealed Classes?
A sealed class is declared using the sealed modifier. Unlike regular classes, a sealed class can only be subclassed within the same file where it is declared. This restriction allows the compiler to know all possible subclasses and enabling Kotlin to perform exhaustive checks when you use sealed classes in when statements.In essence, sealed classes give you a way to define a closed set of types, similar to enum types, but with more flexibility.
Let's make a simple example
Below we will represent the different states of a Request Result:
sealed class RequestResult {
data class Success(val data: String) : Result()
data class Error(val errorMessage: String) : Result()
object Loading : Result()
}
RequestResult is a sealed class and Success, Error, and Loading are all subclasses that represent the possible states.
We can use above sealed class like below:
fun handleResult(result: RequestResult) {
when (result) {
is RequestResult.Success -> println("Data: ${result.data}")
is RequestResult.Error -> println("Error: ${result.errorMessage}")
is RequestResult.Loading -> println("Loading...")
}
}
Notice how the when expression does not require an else branch because Kotlin knows all subclasses of RequestResult class, so the compiler ensures that all cases are covered.
What are the Advantages Sealed Classes:
When to use Sealed Classes
Sealed Interfaces
These work just like sealed classes instead of providing implementations, they define contracts that implementations must fulfill. This pattern is useful when multiple independent types need to represent different states but shouldn’t share implementation details
Below you can see an example of Sealed interface definition:
sealed interface UiState
data class Loading(val progress: Int) : UiState
data class Display(val content: String) : UiState
data class Error(val error: String) : UiState
Nested Sealed Classes
You can nest sealed classes within other classes to create localized hierarchies. For example, you might have sealed classes inside a ViewModel to represent UI events.
class LoginViewModel {
sealed class LoginEvent {
object LoginStarted : LoginEvent()
data class LoginSuccess(val userName: String) : LoginEvent()
data class LoginFailed(val reason: String) : LoginEvent()
}
fun onLogin(event: LoginEvent) {
when (event) {
is LoginEvent.LoginStarted -> println("Logging in...")
is LoginEvent.LoginSuccess -> println("Welcome ${event.userName}")
is LoginEvent.LoginFailed -> println("Login failed: ${event.reason}")
}
}
}
As you can see above, all possible login events are clearly defined within the LoginEvent sealed class. The compiler enforces completeness when handling events.
Let's compare Sealed Classes and Enums
| Feature | Sealed Class | Enum |
|---|---|---|
| Data per type | Can hold arbitrary data (via data classes) | Limited to fixed constants |
| Subclass flexibility | Supports inheritance and complex hierarchies | Each enum constant is a singleton |
| Comprehensiveness | Checked by compiler | Checked by compiler |
| Use cases | Represent states and results | Represent fixed set of constant values |
Combining Sealed Classes With Generics
You can also use generics with sealed classes for type safety and flexibility. This is a very common pattern in Kotlin-based applications—especially in frameworks like Android’s ViewModel or networking layers using coroutines
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val exception: Throwable) : Result<Nothing>()
}
fun <T> processResult(result: Result<T>) {
when (result) {
is Result.Success -> println("Processed data: ${result.data}")
is Result.Failure -> println("Error: ${result.exception.message}")
}
}
Now lets make a Real-World Example
Here we are going to build an API response handler sealed class with a template. Here’s how sealed classes can make your code cleaner in an API-driven app:
// Define a Sealed class with template as Api response with states.
sealed class ApiResponse<out T> {
data class Success<out T>(val data: T) : ApiResponse<T>()
data class Error(val message: String) : ApiResponse<Nothing>()
object Loading : ApiResponse<Nothing>()
}
// Define a suspend function which will perform api call at background with coroutines.
suspend fun fetchUserData(): ApiResponse<String> {
return try {
ApiResponse.Success("User data received!")
} catch (e: Exception) {
ApiResponse.Error(e.message ?: "Unknown error")
}
}
After we define above structure, in below code block we will use above sealed class to perform api call and check the result in when function.
val response = fetchUserData()
when (response) {
is ApiResponse.Success -> println("Data: ${response.data}")
is ApiResponse.Error -> println("Error occurred: ${response.message}")
ApiResponse.Loading -> println("Fetching...")
}
Final Words
Sealed classes in Kotlin are an elegant and type-safe way to represent restricted hierarchies. They improve code readability and comprehensiveness. They prevent runtime errors caused by unhandled states.That is all.
Burak Hamdi TUFAN