Introduction
In Kotlin, sealed classes are a powerful feature used to define restricted class hierarchies. A sealed class allows you to define a closed set of subclasses, which is helpful in scenarios where you want to limit the class inheritance tree to a known, fixed set of types. Sealed classes are typically used in state machines, data representation, and error handling, where all possible types or states are known in advance.
In this post, we’ll explore:
✅ What Are Kotlin Sealed Classes?
✅ Creating Sealed Classes
✅ Advantages of Sealed Classes
✅ Working with Sealed Classes and When Expressions
✅ Best Practices for Sealed Classes
By the end of this post, you’ll be able to utilize sealed classes to build more controlled and maintainable code.
1. What Are Kotlin Sealed Classes?
A sealed class is a class that can have a limited set of subclasses. Unlike regular classes, which can have any number of subclasses, sealed classes are closed, meaning that all subclasses must be defined within the sealed class file. This gives you better control over class hierarchies and is often used to model restricted states or fixed types.
Sealed Class vs. Regular Class
In a normal class hierarchy, you can extend a class from anywhere in the project, which can lead to unwanted subclasses. With sealed classes, all subclasses must be defined within the same file, making the hierarchy more predictable and controlled.
2. Creating Sealed Classes
The syntax for creating a sealed class is simple. You define a class using the sealed
keyword, and then you define its subclasses within the same file.
Basic Syntax of a Sealed Class
Here’s the syntax for creating a sealed class:
sealed class Result
class Success(val message: String) : Result()
class Error(val errorMessage: String) : Result()
In this example, Result
is a sealed class, and Success
and Error
are its subclasses. Both subclasses are confined to the same file as the sealed class itself.
Example of Using Sealed Classes
Let’s create a sealed class to represent the result of a network request:
sealed class NetworkResult
data class Success(val data: String) : NetworkResult()
data class Error(val message: String) : NetworkResult()
object Loading : NetworkResult()
Here, NetworkResult
is sealed, and its possible states are:
- Success (contains data)
- Error (contains an error message)
- Loading (an object to indicate the loading state)
3. Advantages of Sealed Classes
Sealed classes provide several advantages over regular classes or enums:
1. Restricted Inheritance
With sealed classes, all subclasses must be defined within the same file, limiting inheritance and ensuring that you know all possible subclasses at compile time. This makes the hierarchy more predictable.
2. Improved Code Safety
Since the compiler knows all possible subclasses, you can use when
expressions without needing an else
branch. If you add a new subclass to the sealed class, the compiler will warn you if you haven’t updated the when
expression to account for it.
3. Expressiveness
Sealed classes allow you to model fixed sets of states or outcomes more naturally. They are useful for representing states in UI-related scenarios (e.g., loading, success, error states) or handling different types of events (e.g., success and failure in network requests).
4. Better Maintenance
Since sealed classes are defined in a single file, they provide a single point of modification. Adding or removing subclasses becomes easier, and it helps with maintaining the code over time.
4. Working with Sealed Classes and When Expressions
One of the most powerful features of sealed classes is the ability to use them with when
expressions. Since the compiler knows all possible subclasses, you can handle them explicitly without needing an else
clause.
Example of Using Sealed Classes with When
Let’s consider the NetworkResult
example from earlier. We can use a when
expression to handle different states:
fun handleNetworkResult(result: NetworkResult) {
when (result) {
is Success -> println("Success: ${result.data}")
is Error -> println("Error: ${result.message}")
Loading -> println("Loading...")
}
}
Since NetworkResult
is sealed, the when
expression handles all possible subclasses explicitly:
Success
holds data.Error
holds a message.Loading
is a singleton object with no data.
You don’t need an else
branch because all possible subclasses are already covered.
Handling New Subclasses
If you add a new subclass to a sealed class and forget to update the when
expression, the compiler will give you an error, ensuring that all cases are covered. For example:
sealed class NetworkResult
data class Success(val data: String) : NetworkResult()
data class Error(val message: String) : NetworkResult()
object Loading : NetworkResult()
data class Timeout(val reason: String) : NetworkResult() // New subclass
fun handleNetworkResult(result: NetworkResult) {
when (result) {
is Success -> println("Success: ${result.data}")
is Error -> println("Error: ${result.message}")
Loading -> println("Loading...")
}
}
The compiler will give an error because the new Timeout
subclass isn’t handled.
5. Best Practices for Sealed Classes
Sealed classes can make your code more concise, maintainable, and less error-prone, but it’s important to follow some best practices.
Best Practice 1: Use Sealed Classes for Known, Fixed Set of States
Sealed classes are best suited for situations where you know all possible states or types in advance, such as:
- UI states (loading, success, error)
- Network response states (success, failure, in-progress)
- Error handling (specific error types)
Best Practice 2: Avoid Overuse
While sealed classes are useful, overusing them can make the code more complex than necessary. If the number of possible subclasses becomes large or dynamic, a regular class or enum may be a better choice.
Best Practice 3: Use Sealed Classes for Better when
Exhaustiveness
By using sealed classes, you can leverage the when
expression to handle all possible cases without relying on an else
branch. This ensures that all cases are explicitly managed, making your code safer and easier to maintain.
Best Practice 4: Group Subclasses Logically
When defining subclasses, make sure they are logically related to the sealed class. This maintains readability and clarity, especially when the sealed class represents a complex state machine.
Conclusion
In this post, you’ve learned:
✅ What sealed classes are and how to define them in Kotlin.
✅ The benefits of using sealed classes, including restricted inheritance, improved safety, and better maintainability.
✅ How to work with sealed classes and when expressions to handle different states.
✅ Best practices for using sealed classes to model known sets of states or types.
Sealed classes are a powerful tool in Kotlin for writing more predictable, maintainable, and robust code. By using them to represent fixed sets of states or outcomes, you can enhance the quality and readability of your code.
🎯 Next Post: Kotlin Coroutines – Simplifying Asynchronous Programming