-
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
195: Practice Exercise: Refactoring to Use Sealed Classes for State
I've seen this pattern a hundred times in code reviews: a developer creates a state class for a screen, and it starts with one or two booleans. Then they realize they need an error message, so they add a nullable string. By the time the feature is "done," they have a data class that looks like a checklist of every possible thing that could happen, and the logic to handle it is a fragile tower of if-else statements.
The "Boolean Soup" Approach
Let's look at a common example. Imagine we're building a user profile screen. The naive way to track the state is to bundle everything into one class and use nulls or flags to signal what's currently happening:
data class ProfileState(
val isLoading: Boolean = false,
val user: User? = null,
val error: String? = null
)
At first glance, this seems flexible. But here's the problem: you've just created a state space where "impossible" states are perfectly legal. What happens if isLoading is true, but user is also populated, and error contains a message? Which one wins? You'll find yourself writing code like if (state.isLoading) { ... } else if (state.error != null) { ... } else if (state.user != null) { ... }. This is where bugs hide. If you change the order of those if-statements, you might accidentally hide an error message because a stale user object was still hanging around in the state.
Making Impossible States Unrepresentable
The better way—and the way I want you to lean into—is using sealed classes. Instead of one class with many optional fields, we define a closed hierarchy where only one state can exist at a time. We aren't just organizing the data; we're using the type system to forbid the "Boolean Soup" I mentioned earlier.
sealed class ProfileState {
object Loading : ProfileState()
data class Success(val user: User) : ProfileState()
data class Error(val message: String) : ProfileState()
}
Now, it is physically impossible to be in the Loading state and the Success state simultaneously. The User object doesn't even exist in the Loading or Error types. I love this approach because it moves the burden of correctness from my memory (or a long list of unit tests) to the compiler.
The Power of Exhaustive Checking
The real magic happens when you actually use this state in your UI logic. When you use a when expression with a sealed class, Kotlin knows exactly how many subclasses exist. If you forget to handle the Error state, the code won't even compile.
fun render(state: ProfileState) {
when (state) {
is ProfileState.Loading -> showSpinner()
is ProfileState.Success -> showUser(state.user)
is ProfileState.Error -> showError(state.message)
}
}
Compare this to the naive approach where you'd probably have a trailing else block that just logs a "should not happen" error. With sealed classes, the else block becomes unnecessary. If I add a new state later—say, ProfileState.Empty for users with no data—the compiler will immediately point to every single when block in the entire project and tell me, "Hey, you forgot to handle the Empty state here." That's the kind of safety net that lets you refactor large codebases without the constant fear of breaking a random edge case.
📋 Practical Task
Refactor the Order Tracking State
You have been handed a legacy piece of code for a food delivery app. The OrderState is currently implemented using the "Boolean Soup" method, making the UI logic messy and error-prone. Your task is to refactor this into a sealed class hierarchy.
Current Naive Implementation:
data class OrderState(
val isPending: Boolean = false,
val isCooking: Boolean = false,
val isOutForDelivery: Boolean = false,
val isDelivered: Boolean = false,
val orderId: String? = null,
val driverName: String? = null,
val errorMessage: String? = null
)
Your Requirements:
- Create a sealed class named
OrderState. - Define a
Pendingstate (should contain theorderId). - Define a
Cookingstate (should contain theorderId). - Define a
OutForDeliverystate (should contain theorderIdanddriverName). - Define a
Deliveredstate (should contain theorderId). - Define an
Errorstate (should contain theerrorMessage). - Implement a function
handleOrderState(state: OrderState)that uses awhenexpression to print a specific message for each state (e.g., "Your order [id] is being cooked!").
There are no comments for now.