DEV Community

Cover image for Sealed Interfaces vs. Sealed Classes in Kotlin: When and Why to Use Each
Valerii Popov
Valerii Popov

Posted on

Sealed Interfaces vs. Sealed Classes in Kotlin: When and Why to Use Each

Kotlin’s sealed classes are widely used to create closed hierarchies, especially when representing UI states or other fixed types. But with the introduction of sealed interfaces in Kotlin 1.5, developers now have another powerful tool for structuring type-safe hierarchies. Both allow for exhaustive when expressions and ensure type safety, but they serve slightly different purposes and shine in different contexts. Let’s explore when and why to use sealed classes versus sealed interfaces in Kotlin, with practical examples, including Android UI components.

Sealed Classes in Kotlin

Sealed classes are ideal when you need a limited set of subclasses that represent specific states, often within a single context. A sealed class hierarchy allows you to create a closed set of subclasses—meaning no other classes outside this file can extend the sealed class. This is especially useful for representing states in a when expression, ensuring every possible subclass is accounted for at compile time.

Example: UI State Representation with Sealed Classes
In Android development, sealed classes are a common choice for handling UI state. Let’s imagine a simple loading screen that can display three possible states: Loading, Success, or Error. Here’s how you might implement this with a sealed class:

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

fun handleUiState(uiState: UiState) {
    when (uiState) {
        is UiState.Loading -> showLoadingSpinner()
        is UiState.Success -> showData(uiState.data)
        is UiState.Error -> showError(uiState.message)
    }
}

fun showLoadingSpinner() { /* Show loading spinner */ }
fun showData(data: String) { /* Display data */ }
fun showError(message: String) { /* Show error message */ }
Enter fullscreen mode Exit fullscreen mode

In this example:

The UiState sealed class represents a closed hierarchy where only specific states (Loading, Success, and Error) are valid.
The when expression in handleUiState ensures that all states are handled exhaustively, giving us compile-time safety.
Sealed classes are well-suited to representing UI states because they provide clear and structured representations of finite states, making it easier to reason about the different UI scenarios.

Sealed Interfaces in Kotlin

Sealed interfaces, introduced in Kotlin 1.5, add flexibility to sealed hierarchies. Unlike sealed classes, which enforce a single superclass, sealed interfaces allow you to combine multiple types, as Kotlin supports multiple interfaces but only single inheritance for classes. This makes sealed interfaces a great choice for flexible hierarchies where shared behavior is needed across unrelated types.

Example: Shared ViewHolder Types with Sealed Interfaces
Let’s say we’re building an app that displays different items in a RecyclerView list, and each item type has a unique ViewHolder. We could use sealed interfaces to define shared behavior among ViewHolders, allowing flexibility while ensuring that only specific types implement the interface.

sealed interface ListItem

data class TextItem(val text: String) : ListItem
data class ImageItem(val imageUrl: String) : ListItem
data class VideoItem(val videoUrl: String) : ListItem

abstract class ListItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    abstract fun bind(item: ListItem)
}

class TextItemViewHolder(view: View) : ListItemViewHolder(view) {
    override fun bind(item: ListItem) {
        if (item is TextItem) {
            // Bind text item data
        }
    }
}

class ImageItemViewHolder(view: View) : ListItemViewHolder(view) {
    override fun bind(item: ListItem) {
        if (item is ImageItem) {
            // Bind image item data
        }
    }
}

class VideoItemViewHolder(view: View) : ListItemViewHolder(view) {
    override fun bind(item: ListItem) {
        if (item is VideoItem) {
            // Bind video item data
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

The ListItem sealed interface defines a set of allowed item types (TextItem, ImageItem, VideoItem).
Each item type is flexible and can be combined with other interfaces if needed, making the sealed interface versatile.
ListItemViewHolder subclasses each handle a specific type of item, enabling shared behavior without enforcing a strict hierarchy.
This structure is especially useful for RecyclerView, where flexibility and type-safety are often needed to handle diverse item types.

Choosing Between Sealed Classes and Sealed Interfaces

Understanding when to use sealed classes versus sealed interfaces largely depends on the use case:

Use Sealed Classes for fixed, finite states where each subclass represents a distinct, unchangeable state. They work well in cases like UI state handling, where each possible state (e.g., loading, success, error) is predefined and must be accounted for.

Use Sealed Interfaces when you need flexibility and want to share behavior across different types that aren’t necessarily in a strict inheritance hierarchy. Sealed interfaces are ideal for cases where multiple types share behavior but also may combine with other interfaces or classes.

Performance and Compatibility Considerations

Since sealed classes and sealed interfaces are both evaluated at compile-time for exhaustiveness, they don’t introduce additional runtime overhead, making them performant. However, it’s worth noting that:

  • Sealed Classes: Each subclass must be nested or within the same file as the sealed class, limiting their flexibility.
  • Sealed Interfaces: Since they support multiple inheritance, sealed interfaces are more versatile but should be used cautiously to avoid creating overly complex hierarchies.

Conclusion

Sealed classes and sealed interfaces both bring powerful type safety and code clarity to Kotlin. By understanding when to use each, you can build more robust, flexible, and readable code structures. Whether you’re representing fixed UI states with sealed classes or creating flexible hierarchies for items in a list with sealed interfaces, Kotlin’s sealed types offer a range of options to support your architecture.

Choosing the right approach can significantly improve the maintainability and readability of your code, making your Kotlin projects more robust and reliable in the long run.

Top comments (0)