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
- Compile-Time Safety: The compiler ensures all cases are handled, reducing the chances of runtime errors.
- Readability: Both sealed classes and
when
statements provide a clear and concise way to represent complex logic. - Maintainability: Adding new states or cases is straightforward and less error-prone.
- Type-Specific Behavior: Each subclass can carry its own data and behavior, making it easier to handle diverse scenarios.
Best Practices
- Keep Sealed Classes in a Single File: This enforces the finite hierarchy and improves code organization.
- Leverage Data Classes: Use data classes as subclasses to store additional information.
- Avoid Overloading: Keep the hierarchy simple and avoid excessive nesting.
- 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!