Skip to main content

Kotlin Sealed Classes

Introduction

Sealed classes are a powerful feature in Kotlin that represent restricted class hierarchies. In these hierarchies, a value can have a type from a limited set of types, and no other type. They're somewhat similar to enum classes but more flexible, as each subclass can have multiple instances along with their own state.

Sealed classes are particularly useful when you're working with states that can only be one of several well-defined types, making them perfect for representing limited hierarchies like network responses, UI states, or result types.

What Are Sealed Classes?

A sealed class is a class that restricts the possibility of inheritance. All direct subclasses of a sealed class must be declared in the same file as the sealed class itself. This constraint allows the compiler to verify that all possible subclasses are known at compile time, enabling exhaustive checks in when expressions.

Basic Syntax

kotlin
sealed class Result {
data class Success(val data: Any) : Result()
data class Error(val message: String, val exception: Exception? = null) : Result()
object Loading : Result()
object Empty : Result()
}

Key Features of Sealed Classes

1. Restricted Hierarchy

All subclasses of a sealed class must be defined in the same source file where the sealed class is defined. This restriction ensures that the compiler knows all possible types at compile time.

2. Abstract by Default

Sealed classes are abstract by default. They cannot be instantiated directly, only through their subclasses.

kotlin
sealed class Shape {
// Cannot create instance of Shape directly
abstract fun area(): Double
}

class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
}

class Rectangle(val width: Double, val height: Double) : Shape() {
override fun area() = width * height
}

class Triangle(val base: Double, val height: Double) : Shape() {
override fun area() = 0.5 * base * height
}

3. Exhaustive When Statements

One of the most powerful features of sealed classes is that they enable exhaustive checking in when expressions. When you use a sealed class in a when statement, the compiler can verify that all cases are covered:

kotlin
fun getShapeInfo(shape: Shape): String {
return when (shape) {
is Circle -> "Circle with radius ${shape.radius} and area ${shape.area()}"
is Rectangle -> "Rectangle with width ${shape.width}, height ${shape.height} and area ${shape.area()}"
is Triangle -> "Triangle with base ${shape.base}, height ${shape.height} and area ${shape.area()}"
// No 'else' branch needed - compiler knows all possible subclasses
}
}

4. Combining with Data Classes and Objects

Sealed classes work particularly well with data classes and objects, allowing you to create a rich type hierarchy:

kotlin
sealed class ApiResponse<T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error<T>(val code: Int, val message: String) : ApiResponse<T>()
data class Loading<T>(val progress: Float? = null) : ApiResponse<T>()
object Empty : ApiResponse<Nothing>()
}

fun <T> handleResponse(response: ApiResponse<T>) {
when (response) {
is ApiResponse.Success -> println("Data received: ${response.data}")
is ApiResponse.Error -> println("Error: ${response.code} - ${response.message}")
is ApiResponse.Loading -> {
val progress = response.progress
if (progress != null) {
println("Loading... ${progress * 100}%")
} else {
println("Loading...")
}
}
ApiResponse.Empty -> println("No data available")
}
}

Sealed Classes vs Enums

While sealed classes and enums might seem similar, they have different purposes and capabilities:

FeatureSealed ClassesEnums
State per instanceCan have different stateEach instance has the same properties
Multiple instancesEach subclass can have multiple instancesFixed number of instances
Constructor parametersEach subclass can have its own parametersAll instances share the same parameter structure
FlexibilityMore flexibleMore constrained
Use caseComplex hierarchies with different behaviorsSimple set of related constants

When to Use Sealed Classes

Sealed classes are ideal in the following situations:

  1. Representing a limited set of possible states: Like UI states (loading, success, error)
  2. Result wrappers: For handling success/failure scenarios
  3. Expression trees: When building domain-specific languages or parsers
  4. Event hierarchies: For handling various types of events in your application

Practical Example: UI State Management

Let's see how sealed classes can be used to manage UI states in an application:

kotlin
sealed class UiState {
object Initial : UiState()
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}

class ItemsViewModel {
// Mutable state that the UI will observe
private val _state = MutableLiveData<UiState>(UiState.Initial)
val state: LiveData<UiState> = _state

fun loadItems() {
_state.value = UiState.Loading
// Simulate network request
try {
// Pretend we got data from somewhere
val items = fetchItems()
_state.value = if (items.isEmpty()) {
UiState.Error("No items found")
} else {
UiState.Success(items)
}
} catch (e: Exception) {
_state.value = UiState.Error("Failed to load items: ${e.message}")
}
}

private fun fetchItems(): List<Item> {
// Simulate network request
return listOf(Item("Item 1"), Item("Item 2"))
}
}

// In your UI
fun renderState(state: UiState) {
when (state) {
UiState.Initial -> showInitialUi()
UiState.Loading -> showLoadingIndicator()
is UiState.Success -> displayItems(state.data)
is UiState.Error -> showErrorMessage(state.message)
}
}

Another Practical Example: Parsing JSON

Sealed classes are great for handling different shapes of data that might come from external sources like JSON:

kotlin
sealed class ConfigElement {
data class TextElement(val text: String, val style: TextStyle) : ConfigElement()
data class ImageElement(val url: String, val size: Size) : ConfigElement()
data class ButtonElement(val text: String, val action: String) : ConfigElement()
data class ContainerElement(val elements: List<ConfigElement>) : ConfigElement()
}

fun parseJsonToConfigElement(json: JsonElement): ConfigElement {
return when (json.getString("type")) {
"text" -> ConfigElement.TextElement(
json.getString("content"),
parseTextStyle(json.getObject("style"))
)
"image" -> ConfigElement.ImageElement(
json.getString("url"),
Size(json.getInt("width"), json.getInt("height"))
)
"button" -> ConfigElement.ButtonElement(
json.getString("label"),
json.getString("actionId")
)
"container" -> {
val childElements = json.getArray("children").map { parseJsonToConfigElement(it) }
ConfigElement.ContainerElement(childElements)
}
else -> throw IllegalArgumentException("Unknown element type: ${json.getString("type")}")
}
}

fun renderConfigElement(element: ConfigElement) {
when (element) {
is ConfigElement.TextElement -> renderText(element.text, element.style)
is ConfigElement.ImageElement -> renderImage(element.url, element.size)
is ConfigElement.ButtonElement -> renderButton(element.text, element.action)
is ConfigElement.ContainerElement -> {
// Render container and then recursively render children
element.elements.forEach { renderConfigElement(it) }
}
}
}

Common Patterns with Sealed Classes

The Result Pattern

One of the most common uses of sealed classes is representing operation results:

kotlin
sealed class Result<out T> {
data class Success<out T>(val value: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
}

fun divide(a: Int, b: Int): Result<Int> {
return if (b == 0) {
Result.Failure(IllegalArgumentException("Division by zero"))
} else {
Result.Success(a / b)
}
}

// Usage
fun main() {
val result = divide(10, 2)
when (result) {
is Result.Success -> println("Result: ${result.value}") // Prints: Result: 5
is Result.Failure -> println("Error: ${result.error.message}")
}

val errorResult = divide(10, 0)
when (errorResult) {
is Result.Success -> println("Result: ${errorResult.value}")
is Result.Failure -> println("Error: ${errorResult.error.message}") // Prints: Error: Division by zero
}
}

Nested Sealed Classes

You can also nest sealed classes to create more complex hierarchies:

kotlin
sealed class NetworkEvent {
sealed class Connection : NetworkEvent() {
object Connected : Connection()
object Disconnected : Connection()
data class ConnectionError(val reason: String) : Connection()
}

sealed class Data : NetworkEvent() {
data class Received(val data: ByteArray) : Data()
data class Sent(val size: Int) : Data()
data class Error(val exception: Exception) : Data()
}
}

fun handleNetworkEvent(event: NetworkEvent) {
when (event) {
is NetworkEvent.Connection.Connected -> println("Connected to server")
is NetworkEvent.Connection.Disconnected -> println("Disconnected from server")
is NetworkEvent.Connection.ConnectionError -> println("Connection error: ${event.reason}")
is NetworkEvent.Data.Received -> println("Received ${event.data.size} bytes")
is NetworkEvent.Data.Sent -> println("Sent ${event.size} bytes")
is NetworkEvent.Data.Error -> println("Data error: ${event.exception.message}")
}
}

Limitations of Sealed Classes

While sealed classes are powerful, they have some limitations:

  1. All subclasses must be in the same file: This can lead to large files if you have many subclasses.
  2. No new subclasses can be added outside the file: This makes sealed classes less extensible than regular abstract classes.
  3. Increased boilerplate: Compared to enums, sealed classes require more code to define.

Summary

Sealed classes in Kotlin provide a powerful way to represent restricted class hierarchies. They combine the benefits of enums with the flexibility of class hierarchies, allowing each subclass to have its own state and behavior.

Key takeaways:

  • Sealed classes restrict inheritance to a known set of subclasses
  • They enable exhaustive checks in when expressions without needing an else branch
  • They work well with data classes and object declarations
  • They're perfect for representing limited hierarchies like states, results, or events
  • All subclasses must be defined in the same file as the sealed class

By using sealed classes in your Kotlin code, you can create more expressive, type-safe, and maintainable code, particularly when working with different states or variants of a concept.

Exercises

  1. Create a sealed class hierarchy to represent different shapes (Circle, Rectangle, Triangle) with methods to calculate area and perimeter for each shape.
  2. Implement a sealed class to represent different types of notifications in an app (Message, Alert, Update).
  3. Design a Result sealed class for a network operation that can be Success, Error, or Loading, then use it in a function that simulates an API call.
  4. Create a nested sealed class hierarchy to represent a file system with different types of files and directories.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)