In Kotlin, sealed classes are beneficial when you have a fixed set of types that a value can belong to, and you want to enforce this constraint at compile time. Sealed classes improve safety and clarity by ensuring your program handles every possible subclass within a defined hierarchy. Let's explore some practical use cases of sealed classes in Kotlin, accompanied by code examples.
What are Sealed Classes?
A sealed class is an abstract class that can only be subclassed within the same package. The Kotlin compiler knows all the subclasses at compile-time, which makes it easier to handle all instances of the sealed class appropriately. In this way, sealed classes allow you to define restricted class hierarchies that provide more guarantees than regular inheritance.
sealed class Operation {
class Add(val value: Int) : Operation()
class Subtract(val value: Int) : Operation()
class Multiply(val value: Int) : Operation()
class Divide(val value: Int) : Operation()
}
In this example, Operation is a sealed class which allows for an explicitly restricted set of actions a calculator can perform. You cannot add a new operation elsewhere in the code base without modifying the original sealed class declaration.
Sealed Classes for Handling UI State
Sealed classes are particularly useful when representing UI states, such as loading, success, or error states. This pattern is common when dealing with network requests.
sealed class UiState {
object Loading : UiState()
data class Success<T>(val data: T) : UiState()
data class Error(val exception: Throwable) : UiState()
}
In this example, we have a sealed class UiState that represents three states: Loading, Success, and Error. The object Loading represents a loading state, whereas Success wraps the successful data response, and Error wraps an exception that may have occurred.
Sealed Classes in Expression Evaluation
Another real-world application of sealed classes is in representing expressions in a type-safe manner. By defining the different types of expressions as subclasses of a sealed class, evaluation and transformation become more robust and error-proof.
sealed class Expr {
data class Const(val number: Double) : Expr()
data class Sum(val left: Expr, val right: Expr) : Expr()
data class Mult(val left: Expr, val right: Expr) : Expr()
}
fun eval(expr: Expr): Double = when (expr) {
is Expr.Const -> expr.number
is Expr.Sum -> eval(expr.left) + eval(expr.right)
is Expr.Mult -> eval(expr.left) * eval(expr.right)
}
In this example, an expression can be a constant, a sum, or a multiplication. The sealed class Expr ensures that any additional types of expressions must be explicitly handled within the eval function, providing enhanced compile-time checks.
By utilizing sealed classes like this, your codebase benefits from improved type safety and readability, as programmers can quickly understand every subclass being used under a particular sealed class, without having to guess about unlisted implementations.