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:
-
HeaderItem
for section titles -
StoryItem
for news content -
AdItem
for 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 interface
when modeling hierarchies. It’s flexible and plays nicely with external APIs. -
Use
sealed class
when:- You need shared state or behavior in the base
- You want to restrict instantiation (e.g.,
private
constructor) - 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)