Sealed Classes and When Statements in Kotlin: A Comprehensive Guide

Kotlin, with its modern features and concise syntax, has become the go-to programming language for Android development and beyond. Among its numerous powerful features, sealed classes and when statements stand out as tools that significantly improve code readability, maintainability, and safety. In this blog post, we’ll dive deep into sealed classes and when statements, explore their synergy, and illustrate their practical use cases with real-world examples.


What Are Sealed Classes in Kotlin?

Sealed classes are a special kind of class in Kotlin that are used to represent a restricted hierarchy. Unlike regular classes, they allow you to define a finite set of subclasses within the same file. This feature makes them ideal for modeling scenarios where a type can have a limited set of possible states.

Key Characteristics of Sealed Classes:

  • Restricted Subclassing: Subclasses of a sealed class must be defined in the same file as the sealed class declaration.
  • Type Safety: The compiler knows all possible subclasses at compile time, enabling exhaustive checks.
  • Improved Readability: They make code more readable by clearly representing all possible states.

Syntax:

sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val width: Double, val height: Double) : Shape()
    object Triangle : Shape()
}

In this example, the Shape class is sealed, and its subclasses Circle, Rectangle, and Triangle are defined within the same file. This hierarchy explicitly defines all possible shapes.


The Power of When Statements

The when statement in Kotlin is a more powerful and expressive alternative to the traditional switch statement found in other languages. When used with sealed classes, it shines even brighter by enabling exhaustive pattern matching.

Key Features of When Statements:

  • Pattern Matching: Matches values or types against a set of patterns.
  • Exhaustive Checks: Ensures all possible cases are handled when used with sealed classes.
  • Concise Syntax: Reduces boilerplate code while improving readability.

Syntax:

fun describeShape(shape: Shape): String = when (shape) {
    is Shape.Circle -> "Circle with radius ${shape.radius}"
    is Shape.Rectangle -> "Rectangle with width ${shape.width} and height ${shape.height}"
    Shape.Triangle -> "Triangle"
    // No need for an `else` branch as all cases are covered.
}

Combining Sealed Classes and When Statements

The true power of sealed classes and when statements emerges when they are used together. Let’s explore some real-world scenarios to illustrate their synergy.

1. Modeling UI States in an Application

Sealed classes are perfect for representing different states in a UI, such as loading, success, and error.

Example:

sealed class UiState {
    object Loading : UiState()
    data class Success(val data: String) : UiState()
    data class Error(val message: String) : UiState()
}

fun renderUi(state: UiState): String = when (state) {
    is UiState.Loading -> "Loading..."
    is UiState.Success -> "Data: ${state.data}"
    is UiState.Error -> "Error: ${state.message}"
}

// Usage
val currentState: UiState = UiState.Success("Welcome to Kotlin!")
println(renderUi(currentState))

This approach ensures that all states are accounted for, preventing runtime errors caused by unhandled cases.

2. Handling API Responses

Sealed classes and when statements can simplify error handling and response parsing in API calls.

Example:

sealed class ApiResponse<out T> {
    data class Success<T>(val data: T) : ApiResponse<T>()
    data class Error(val errorCode: Int, val message: String) : ApiResponse<Nothing>()
    object Loading : ApiResponse<Nothing>()
}

fun handleApiResponse(response: ApiResponse<String>) = when (response) {
    is ApiResponse.Success -> "Data received: ${response.data}"
    is ApiResponse.Error -> "Error ${response.errorCode}: ${response.message}"
    ApiResponse.Loading -> "Loading..."
}

// Usage
val response: ApiResponse<String> = ApiResponse.Error(404, "Not Found")
println(handleApiResponse(response))

3. Command Processing

In applications that require processing user commands, sealed classes can represent the command hierarchy.

Example:

sealed class Command {
    object Start : Command()
    object Stop : Command()
    data class SendMessage(val message: String) : Command()
}

fun processCommand(command: Command): String = when (command) {
    Command.Start -> "Starting..."
    Command.Stop -> "Stopping..."
    is Command.SendMessage -> "Sending message: ${command.message}"
}

// Usage
val command: Command = Command.SendMessage("Hello, Kotlin!")
println(processCommand(command))

4. State Management in Finite State Machines

Finite state machines (FSM) can be elegantly modeled using sealed classes.

Example:

sealed class TrafficLight {
    object Red : TrafficLight()
    object Yellow : TrafficLight()
    object Green : TrafficLight()
}

fun nextLight(current: TrafficLight): TrafficLight = when (current) {
    TrafficLight.Red -> TrafficLight.Green
    TrafficLight.Yellow -> TrafficLight.Red
    TrafficLight.Green -> TrafficLight.Yellow
}

// Usage
val currentLight: TrafficLight = TrafficLight.Red
println(nextLight(currentLight))

Advantages of Using Sealed Classes with When

  1. Compile-Time Safety: The compiler ensures all cases are handled, reducing the chances of runtime errors.
  2. Readability: Both sealed classes and when statements provide a clear and concise way to represent complex logic.
  3. Maintainability: Adding new states or cases is straightforward and less error-prone.
  4. Type-Specific Behavior: Each subclass can carry its own data and behavior, making it easier to handle diverse scenarios.

Best Practices

  1. Keep Sealed Classes in a Single File: This enforces the finite hierarchy and improves code organization.
  2. Leverage Data Classes: Use data classes as subclasses to store additional information.
  3. Avoid Overloading: Keep the hierarchy simple and avoid excessive nesting.
  4. Ensure Exhaustiveness: Always use when statements with sealed classes to benefit from compile-time exhaustiveness checks.

Conclusion

Sealed classes and when statements are a match made in Kotlin heaven. They enable developers to write safer, more readable, and maintainable code, especially when dealing with restricted hierarchies and state management. By incorporating these features into your Kotlin projects, you can create robust and expressive codebases that are easier to extend and debug.

Whether you’re managing UI states, handling API responses, or processing commands, sealed classes combined with when statements are tools you’ll reach for time and again. Start using them in your projects today, and experience the Kotlin advantage firsthand!