Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
21: Sealed Classes and Sealed Interfaces
I was working on a payment processing screen the other day and ran into a problem that I think perfectly illustrates why sealed classes exist. I needed a way to represent the different states of the payment flow: waiting for input, processing the transaction, a successful finish, or a failure.
The Enum Wall
My first instinct was to reach for an enum. It's the standard way to handle a fixed set of options, right? I wrote something like this:
enum class PaymentState {
IDLE,
PROCESSING,
SUCCESS,
ERROR
}
That worked fine until I realized that a "Success" state isn't just a label—it needs to carry the transaction ID. And an "Error" state needs to carry an error message or a code. Enums are great, but they are singleton instances. I can't have one ERROR instance that simultaneously holds "Insufficient Funds" for one user and "Timeout" for another. I hit a wall immediately.
The "Else" Headache
So, I pivoted. I thought, "I'll just use a standard class hierarchy." I created a base class and subclassed it for each state. This allowed me to add the data I needed:
open class PaymentState
class Idle : PaymentState()
class Processing : PaymentState()
class Success(val transactionId: String) : PaymentState()
class Error(val message: String) : PaymentState()
Now I have my data. But here is where the friction started. I used a when expression to update the UI based on the state, and the compiler started complaining:
fun updateUI(state: PaymentState) {
when (state) {
is Idle -> showInput()
is Processing -> showLoadingSpinner()
is Success -> showReceipt(state.transactionId)
is Error -> showError(state.message)
// The compiler screams: 'when' expression must be exhaustive!
}
}
Since PaymentState is an open class, the compiler has no way of knowing if some other developer, in some other file, decided to create a RefundedState that inherits from PaymentState. To make the compiler happy, I had to add an else -> {} branch. I hated this. Why? Because if I add a new state to the hierarchy six months from now, the compiler won't warn me that I forgot to handle it in updateUI; it'll just quietly fall into the else block and do nothing. That's a bug waiting to happen.
Closing the Loop with Sealed Classes
This is exactly where sealed comes in. By marking the class as sealed, I'm telling Kotlin: "This class can be subclassed, but only within this same package (and usually this same file)." I'm effectively restricting the hierarchy.
sealed class PaymentState {
object Idle : PaymentState()
object Processing : PaymentState()
data class Success(val transactionId: String) : PaymentState()
data class Error(val message: String) : PaymentState()
}
I switched Idle and Processing to object because they don't hold any unique data—they're just markers. Now, look what happens to my when expression:
fun updateUI(state: PaymentState) {
when (state) {
is PaymentState.Idle -> showInput()
is PaymentState.Processing -> showLoadingSpinner()
is PaymentState.Success -> showReceipt(state.transactionId)
is PaymentState.Error -> showError(state.message)
// No 'else' required!
}
}
The compiler now knows every possible subtype of PaymentState. If I add class Refunded : PaymentState() tomorrow, the updateUI function will immediately turn red, forcing me to handle the new case. It transforms a potential runtime crash into a compile-time requirement. It's a massive safety win.
When to ditch the class for an interface
You'll also see sealed interface in the wild. You might wonder why you'd use one over a sealed class. For me, it usually comes down to two things: memory and flexibility.
A sealed class can have a constructor and hold internal state. A sealed interface cannot. If your states are purely behavioral and don't need to share any common private properties, an interface is "lighter." More importantly, a class can only inherit from one other class. If your state objects need to implement multiple different hierarchies, a sealed interface is your only option.
sealed interface CanBeCancelled
sealed interface PaymentState
class Processing : PaymentState, CanBeCancelled
class Success(val id: String) : PaymentState
// Success cannot be cancelled, but Processing can.
I use sealed classes when I have a clear "is-a" relationship with shared data, and sealed interfaces when I'm defining a closed set of types that might fit into multiple categories.
📋 Practical Task
Build a Network Response Handler
You are building a data layer for a weather app. You need to handle the various results of an API call. Instead of using nulls or throwing exceptions, you will use a sealed hierarchy to represent the outcome.
Requirements:
- Create a sealed class named
NetworkResult. - Create three subtypes:
Success: A data class that holds adata: String(the JSON response).ApiError: A data class that holds anerrorCode: Int.NetworkError: An object (since it doesn't need unique data).
- Write a function called
handleResponse(result: NetworkResult)that uses awhenexpression to print a different message for each state:- Success: "Received data: [data]"
- ApiError: "API failed with code: [errorCode]"
- NetworkError: "No internet connection."
- Ensure the
whenexpression is exhaustive without using anelseblock.
There are no comments for now.