In my last post, we saw how sealed class can shield us from junk-drawer null objects by enforcing compile-time exhaustiveness.
But sealed classes still have their limits. And when I hit that wall, sealed interfaces broke it down.
The Brick Wall: RecyclerView
Imagine a RecyclerView in a news app:
-
HeaderItemfor section titles -
StoryItemfor news content -
AdItemfor in-feed ads
Naturally, I want all items under one umbrella type:
sealed class ContentItem {
data class HeaderItem(val text: String) : ContentItem()
data class StoryItem(val story: Story) : ContentItem()
data class AdItem(val adView: View) : ContentItem()
}
Now my adapter can switch over ContentItem exhaustively.
Beautiful.
Until—an ad library arrives with its own AdView subclass that must extend their base AdItemBase.
Suddenly, my world crumbles.
- Kotlin sealed classes = single inheritance
- Java = single inheritance
- Two base classes → 💥 no dice
I was cornered.
Enter Sealed Interfaces
Kotlin 1.5 introduced sealed interfaces.
Unlike sealed classes, they don’t lock you into one inheritance path. Any class can implement multiple sealed interfaces in addition to extending a base class.
So I refactor:
sealed interface ContentItem
data class HeaderItem(val text: String) : ContentItem
data class StoryItem(val story: Story) : ContentItem
// Can extend base class AND implement ContentItem
class AdItem(val adView: View) : AdItemBase(), ContentItem
Suddenly, the wall is gone. My adapter still enjoys exhaustiveness, and I’m free to mix sealed types with third-party hierarchies.
Why Did Sealed Class Fail Here?
It’s not the sealed keyword itself — it’s that sealed classes still follow the JVM’s single-inheritance rule.
You can’t both:
- Extend a library base class
- And extend your own sealed base class
That’s the trap sealed interfaces escape.
Rule of Thumb
-
Default to
sealed interfacewhen modeling hierarchies. It’s flexible and plays nicely with external APIs. -
Use
sealed classwhen:- You need shared state or behavior in the base
- You want to restrict instantiation (e.g.,
privateconstructor) - You want companion objects or utility functions tied to the sealed root
Takeaway
sealed class gave us type safety.
sealed interface took it a step further, freeing us from the chains of single inheritance.
My rule now? Default to sealed interface. Reach for sealed class only when shared implementation is essential.
Have you ever been cornered by single inheritance in Kotlin? How did you escape? 👇
Top comments (0)