Design Converter
Education
Last updated on Jan 21, 2025
Last updated on Jan 21, 2025
What makes Kotlin so powerful for developers?
If you’ve been exploring Kotlin’s features, you’ve likely come across the Kotlin sealed interface. It’s a handy tool that brings clarity and structure to your code. By allowing you to define restricted hierarchies, it helps you handle specific scenarios with ease.
In this blog, we’ll look at its key benefits and practical use cases.
Let’s simplify what it means and how it works! 🚀
A sealed class in Kotlin is a class that restricts the formation of new subclasses beyond those declared within the same file. This sealed class concept has traditionally helped developers enforce restricted class hierarchies, resulting in readable code and reliable compile time checks. A sealed class allows you to declare subclasses that represent a closed set of variations—no other subclasses can appear outside the file as the sealed type’s definition. Essentially, you know all the subclasses at compile time, which reduces error occurrences and simplifies reasoning about a sealed hierarchy.
In contrast, a sealed interface extends this principle beyond classes alone. A sealed interface is implicitly abstract, meaning it cannot be instantiated directly, and its possible implementations are also restricted. Introduced in kotlin 1.5, a sealed interface can define interfaces that behave somewhat like a sealed class, but offer additional flexibility. Unlike a sealed class, a sealed interface may be extended by different classes across different files within the same module, as long as the subclasses remain known at compile time. This allows for the construction of multiple sealed interfaces that can share common contracts and can be combined to form class hierarchies that scale well, even for large sealed class hierarchies spanning multiple file boundaries within the same package.
When working with classes and sealed interfaces, the fundamental difference lies in the intent and how you define your subclasses and implementations:
A sealed class must have all its new subclasses defined in the same file. This strict encapsulation ensures that new implementations cannot appear out of nowhere, reducing the chance of error and making it easier to reason about the code. Such a sealed class helps maintain fully known type hierarchies, ensuring your when expression branches cover all subclasses.
A sealed interface is defined with a similar sealed modifier. However, its implementations (i.e., implement interfaces that extend it) can reside in different files as long as they remain in the same package, or at least the same compilation unit, and are compiled in the same module. This eases the pain of stuffing everything into one file, enabling you to spread out your subclasses logically. By using a sealed interface, you can have two subclasses or even more other classes implement it, forming flexible yet limited restricted class hierarchies.
A sealed interface can coexist with sealed class, enum classes, and data class definitions to produce finely tuned sealed hierarchy solutions. For example, you can have a sealed interface define a contract for states, and several sealed class implementations representing different states of a data flow. You might further nest data class elements or compare with enum classes, with each subclass capturing unique conditions. This synergy allows better error control at compile time because the compiler can verify if a when expression is exhaustive.
Without a sealed interface, controlling new subclasses across large sealed class hierarchies can be a burden. A sealed interface enables you to split logic into different files while maintaining the same compilation unit, thus reducing error risk and making it simpler to maintain readable code across a large codebase.
By allowing subclasses to spread across the same package or same module, you can organize your implementations more naturally. This yields more modular code and reduces error potential since all subclasses are still known at compile time. Splitting classes and interfaces into multiple sealed interfaces or a combination of sealed class and sealed interface leads to cleaner architecture.
Because the sealed interface is sealed, the compiler ensures that no other classes outside the declared scope can implement it. This is particularly useful in scenarios requiring limited extension, such as building a stable API where val error cases are predefined and cannot be silently extended by unknown implementations. It also ensures that if you create a val value or store a string to represent some data, the possible handlers are fixed and well-defined.
A key benefit is ensuring that your when expression covers all subclasses. If you add a new subclasses to your sealed interface, the compiler forces you to handle that case in your when expression, eliminating runtime exception surprises. In other words, it reduces the risk of error at runtime by pushing correctness checks into compile time.
Example scenarios where a sealed interface truly shines:
Consider an example where you represent different states in an application’s data flow. With a sealed class and a sealed interface, you can define states like Loading, Success, and Failure.
For instance, use a data class as a subclasses that represent a successful result containing a val value of type string or a val error message also of type string. In this scenario, a sealed interface can define common functionality for all states, and a sealed class can refine specific behaviors. Since all states are defined and declared in one compilation unit, no other subclasses can appear from unknown sources, ensuring exhaustive handling in the when expression.
When working with Java interop scenarios or distributing logic across a module boundary, a sealed interface can logically help structure class hierarchies. If your system’s class design involves abstracting behaviors into interfaces, you can implement these interfaces as sealed interface constructs, ensuring error handling and stable object modeling are consistent throughout the system.
If you are designing a library API where only certain new implementations should be allowed, a sealed interface ensures that no unintended object implementations are added. This can prevent future error cases. For instance, a sealed interface can define a contract for data processing. You might have a sealed class hierarchy under it and a few data class entries representing configuration. You can store configuration as a string, constructors defined as data, and ensure only certain object or class instances are permissible. This leads to better maintainability and prevents accidental error conditions at runtime.
Consider an example where you define a sealed interface to represent the result of a network request. This allows a structured approach to handling error, success, and loading states. We will show how a sealed interface combined with a sealed class and data class can improve your readable code:
1// Within the same file and same package 2sealed interface NetworkResult { // sealed interface 3 val message: String 4} 5 6// sealed class that implements the sealed interface 7sealed class ResponseResult( 8 override val message: String 9) : NetworkResult { 10 11 data class Success(val valValue: String) : ResponseResult("Success") // data class and val value 12 data class Failure(val valError: String) : ResponseResult("Error occurred") // data class and val error 13} 14 15// Another sealed class for Loading 16sealed class LoadingStatus : NetworkResult { 17 override val message: String get() = "Loading" 18 object Loading : LoadingStatus() // object implementation 19}
Here, we have a sealed interface NetworkResult and a sealed class ResponseResult extending it. We used a data class Success and Failure to represent outcomes, with val error (represented by valError) and normal string messages. The combination ensures that the compiler checks the when expression for all the subclasses during compile time, reducing chances of error. If we add new subclasses, the compiler forces us to handle them.
Example usage with a when expression:
1fun handleResult(result: NetworkResult) = when (result) { 2 is ResponseResult.Success -> println("Got success data: ${result.valValue}") 3 is ResponseResult.Failure -> println("Got error: ${result.valError}") 4 is LoadingStatus.Loading -> println("Still loading...") 5 // No other subclasses possible without a compile-time error! 6}
If you introduce new subclasses of NetworkResult and forget to handle them, the compiler raises an error, making your code safer and reducing runtime exception occurrences.
While enum classes also restrict new implementations, they do not offer the same extensibility or constructors flexibility as a sealed class or sealed interface. An enum classes approach typically suits simpler use cases. By contrast, a sealed interface can define interfaces with custom constructors, data, and can be extended by other classes that retain limited extensibility. This enhances composability and reduces error chances, since each object you define is known at compile time.
Keep your sealed interface and sealed class in a logical package structure. Although a sealed class might require all its subclasses in the same file, a sealed interface can tolerate spreading implementations across different files in the same package. Just remember that they must remain in the same compilation unit or same module to preserve compile time checks.
With Java Interop, consider carefully how you expose your sealed constructs. While Java cannot fully enforce the sealed constraints, Kotlin ensures error detection at compile time for Kotlin code. If you need to interact with Java, ensure your sealed interface and sealed class design is well thought out.
Over time, if you need to add new subclasses, a sealed interface makes it safer and easier. The compiler ensures that any expression using a when expression over your sealed types must handle all subclasses, prompting you to update your code and preventing error conditions. This helps you maintain stable and consistent class and interface structures.
A Kotlin Sealed Interface provides an elegant and flexible way to build restricted class hierarchies that scale gracefully across multiple sealed interfaces, different files, and a well-structured compilation unit. Compared to a sealed class, a sealed interface grants more freedom in how you implement and organize your interfaces, while still guaranteeing that all classes and implementations are known at compile time.
By combining data class, enum classes, and other classes or interfaces within a sealed hierarchy, you can ensure fewer error occurrences at runtime, better readable code, and a safer, more predictable codebase. Embrace Kotlin Sealed Interface features to tighten control over your class hierarchies, ensure safe constructors, handle exception scenarios gracefully, and produce robust, stable architectures.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.