-
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
139: Practice Exercise: Implementing a Simple State Machine with Sealed Classes
I've seen a lot of developers move from Java or C# to Kotlin and treat sealed classes as just "fancy enums." They aren't. The real power of a sealed class is how it interacts with the when expression to create a compile-time safety net. When you're building a state machine, that safety net is the only thing keeping your production environment from crashing when you add a new state six months from now.
The Silent Failure of the Else Branch
Take a look at this snippet. I'm simulating a simple payment gateway state machine. The developer here used a sealed class, which is a good start, but they made a classic mistake in the state handler.
sealed class PaymentState {
object Pending : PaymentState()
object Processing : PaymentState()
object Completed : PaymentState()
object Failed : PaymentState()
}
fun handlePayment(state: PaymentState) {
when (state) {
is PaymentState.Pending -> println("Waiting for funds...")
is PaymentState.Processing -> println("Verifying transaction...")
is PaymentState.Completed -> println("Payment successful!")
else -> println("Payment is in an unresolved state.")
}
}
At first glance, this looks fine. It compiles, it runs, and it handles the Failed state via the else block. But here is where the bug hides: imagine your project grows, and you need to add a Refunded state. You add object Refunded : PaymentState() to the sealed class.
The code still compiles. The handlePayment function doesn't throw an error. But now, when a payment is refunded, it hits that else block and prints "Payment is in an unresolved state." Your business logic is now broken, but the compiler didn't warn you. You've essentially turned a powerful type-checking tool into a runtime guessing game.
Enforcing Exhaustiveness
The fix is simple but disciplined: never use else with sealed classes unless you truly have an infinite set of possibilities.
By removing the else block, you force the when expression to be exhaustive. If it's used as an expression (assigning to a variable or returning from a function), Kotlin will refuse to compile the code if a single state is missing. Even if used as a statement, most modern IDEs will highlight the when block in red if you've missed a branch.
sealed class PaymentState {
object Pending : PaymentState()
object Processing : PaymentState()
object Completed : PaymentState()
object Failed : PaymentState()
object Refunded : PaymentState() // New state added
}
fun handlePayment(state: PaymentState) {
// Removing 'else' makes this an exhaustive check
when (state) {
is PaymentState.Pending -> println("Waiting for funds...")
is PaymentState.Processing -> println("Verifying transaction...")
is PaymentState.Completed -> println("Payment successful!")
is PaymentState.Failed -> println("Transaction failed. Please retry.")
is PaymentState.Refunded -> println("Funds have been returned to the user.")
}
}
Now, if you add a Cancelled state tomorrow, the compiler will point exactly to the handlePayment function and say, "Hey, you forgot to handle this." I personally find this far more comforting than hunting through logs to find out why a "Refunded" order is being treated as "unresolved."
Remember, sealed classes allow you to attach different data to different states—which we'll use in the exercise—but the real win is this architectural guardrail. You're letting the compiler manage your checklist of requirements.
📋 Practical Task
Exercise: Building a Document Approval Workflow State Machine
You need to implement a state machine for a document approval system. A document can be in one of the following states:
- Draft: No extra data needed.
- UnderReview: Needs to hold the name of the
reviewer(String). - Published: Needs to hold the
publishDate(String). - Rejected: Needs to hold the
reasonfor rejection (String).
Requirements:
- Create a sealed class
DocumentStatewith the four states listed above. Usedata classfor states that require parameters anddata object(orobject) for those that don't. - Write a function
processDocument(state: DocumentState)that uses awhenexpression to print a specific message for each state:- Draft: "Document is still being edited."
- UnderReview: "Document is currently being reviewed by [reviewer]."
- Published: "Document was published on [publishDate]."
- Rejected: "Document was rejected due to: [reason]."
- Crucial: Ensure the
whenexpression is exhaustive. Do not use anelsebranch. - Test your implementation by creating a list of different
DocumentStateinstances and passing them through theprocessDocumentfunction.
There are no comments for now.