Day 22: Enums & Sealed Classes — Modeling “One Of These Things”
Android with Kotlin for Absolute Beginners — Day 22 of the Series

Day 22: Enums & Sealed Classes — Modeling “One Of These Things”
Android with Kotlin for Absolute Beginners — Day 22 of the Series
A Pattern You’ve Been Working Around
For 21 days you’ve represented choices with strings. Day 17’s gradeFor function returned "A", "B", "C", or "F" — all just String values. Day 11’s habit tracker returned status messages like "🔥 You're on fire" based on a percentage.
It works. But there’s a problem hiding in plain sight: the compiler can’t help you.
If you typo "B" as "b" somewhere, the program won’t crash — it’ll silently behave wrong. If a function takes a String parameter “describing the loading state,” nothing stops a caller from passing "banana". You’re representing a fixed set of choices using a type (String) that can hold any value. That mismatch is where bugs hide.
Today we fix that with two tools designed for exactly this problem: enums for a fixed list of named values, and sealed classes for a fixed set of “kinds of things” that can each carry their own data. Both turn runtime confusion into compile-time clarity, and both pair beautifully with the when expression you learned on Day 9.
These are two of the most loved features in modern Kotlin. They sound abstract but they fix a real class of bugs I used to ship constantly in earlier languages. If your code currently uses strings to represent “states” or “types,” today’s article will probably change how you write the next month of code.
What You’ll Learn Today
- ✅ Enums — a fixed set of named values
- ✅ Adding properties and methods to enum values
- ✅ Sealed classes — a fixed set of types, each with its own data
- ✅ The “exhaustive
when" superpower - ✅ When to reach for an enum vs a sealed class
Part 1: Enums
The Simplest Form
An enum (short for “enumeration”) is a type with a fixed, named list of possible values. Here’s the simplest version:
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
fun main() {
val heading = Direction.NORTH
println(heading) // NORTH
println(heading.name) // NORTH
}
Open the Kotlin Playground and try it.
A few things to notice:
enum class Directiondeclares a new typeNORTH, SOUTH, EAST, WESTare the only values aDirectioncan ever hold. There is no fifth option.- By convention, enum values are written in UPPER_CASE, like constants
- Every enum value has a built-in
.nameproperty (returns"NORTH")
The win is in the type system: a function declared as fun move(direction: Direction) can only be called with one of those four values. You can’t pass "north" (a string), you can’t pass 42, you can’t pass null. The compiler enforces it.
Enums with when — The Magic Pairing
Here’s where enums shine. Combine them with when and Kotlin starts catching bugs at compile time:
enum class Direction { NORTH, SOUTH, EAST, WEST }
fun describe(d: Direction): String = when (d) {
Direction.NORTH -> "Heading up"
Direction.SOUTH -> "Heading down"
Direction.EAST -> "Heading right"
Direction.WEST -> "Heading left"
}
fun main() {
println(describe(Direction.NORTH)) // Heading up
println(describe(Direction.WEST)) // Heading left
}
Run it. Notice something important: there’s no else branch in the when. Normally, using when as an expression requires else (Day 9 rule). But with an enum, Kotlin already knows all the possible values — and as long as you cover every one, no else is needed.
This is called an exhaustive when. It’s a small thing with huge consequences:
If you ever add a fifth direction, every
whenblock that handledDirectionwill immediately fail to compile until you add a branch for it.
That’s a feature, not a bug. The compiler is forcing you to think: “you added a new case — where else does that case need handling?” In real Android code, this single feature has caught me countless “oh, I forgot to handle that screen state” bugs before they shipped.
Enums with Properties and Methods
Enum values can carry their own data and behavior. This is where enums get genuinely powerful:
enum class Direction(val degrees: Int) {
NORTH(0),
EAST(90),
SOUTH(180),
WEST(270);
fun opposite(): Direction = when (this) {
NORTH -> SOUTH
SOUTH -> NORTH
EAST -> WEST
WEST -> EAST
}
}
fun main() {
println(Direction.NORTH.degrees) // 0
println(Direction.NORTH.opposite()) // SOUTH
}
A few new things:
- The enum class has a constructor —
(val degrees: Int)— just like a regular class - Each value passes its own value into that constructor:
NORTH(0),EAST(90), etc. - There’s a semicolon after the last value (
WEST(270);) — that’s required when there are methods after the values - Methods live below the enum values. Inside them,
thisrefers to whichever enum value the method is being called on - The
wheninsideopposite()doesn’t needelsebecause every value ofDirectionis covered — exhaustive again
This pattern — enum values carrying related data and behavior — comes up constantly in Android development. Think enum class LogLevel(val priority: Int) { DEBUG(0), INFO(1), WARNING(2), ERROR(3) }. The values know things about themselves.
Built-in Helpers
Every enum class gets a couple of useful methods for free:
val all = Direction.values() // Array of all values, in declaration order
for (d in Direction.values()) {
println(d)
}
val parsed = Direction.valueOf("NORTH") // Get the value by its name string
println(parsed) // NORTH
// Direction.valueOf("nope") // 💥 Throws IllegalArgumentException
values() returns every enum value as an array — useful for iterating. valueOf("...") parses a string back into an enum value, but throws if the string doesn’t match.
Pro tip: in modern Kotlin, you can also use
Direction.entriesinstead ofvalues()— it returns aListinstead of anArray, which integrates better with collection operations likemapandfilter. Either works;entriesis slightly nicer for new code.
Part 2: Sealed Classes
Enums are perfect for a fixed list of named values. But sometimes the choices aren’t just labels — they each carry different data.
Imagine a network result. It could be:
- Loading — no data yet, just a “wait” signal
- Success — carries the actual data (a user object, an article, etc.)
- Error — carries an error message and maybe an error code
These aren’t four equivalent labels — they’re three fundamentally different shapes of data. An enum can’t model that cleanly. A regular String field telling you “which one” loses the type information for the data. What you need is a sealed class.
The Basic Shape
sealed class NetworkResult {
object Loading : NetworkResult()
data class Success(val data: String) : NetworkResult()
data class Error(val message: String, val code: Int) : NetworkResult()
}
Three new things going on:
**sealed class NetworkResult** — declares a parent type. Thesealedkeyword means “the only subtypes of this class are defined right here, in this file.”**object Loading** — anobjectis a “single instance” — there’s only ever oneLoading, since it carries no data. It’s Kotlin’s built-in way to express a singleton. We’ve informally seenobjectbefore; this is its first formal appearance.**data class Success(val data: String)and `data class Error(...)** — these inherit fromNetworkResult` but each carries its own fields. Day 21’s data class pays off here.
Now we can describe a network result with one type that has three concrete shapes:
fun fetchUser(id: Int): NetworkResult {
// Imagine real network logic here
return NetworkResult.Success("User #$id: Anuki")
}
fun main() {
val result: NetworkResult = fetchUser(42)
when (result) {
is NetworkResult.Loading -> println("Still loading...")
is NetworkResult.Success -> println("Got data: ${result.data}")
is NetworkResult.Error -> println("Failed: ${result.message} (code ${result.code})")
}
}
Notice three things about that when:
**is NetworkResult.Loading** — theisoperator checks the type. (You metisbriefly in Day 8’s smart casts.)- Inside each branch, Kotlin “smart casts”
result— in theSuccessbranch, you can accessresult.datadirectly without any explicit casting, because Kotlin knows from theischeck thatresultis specifically aSuccessthere. Same forresult.messagein theErrorbranch. - No
elseneeded. Sealed classes give you the same exhaustivewhensuperpower as enums. The compiler knows all three subtypes are defined in this one file, and forces you to handle every one.
That last point is the whole reason sealed classes exist. They give you the safety of enums with the flexibility of carrying different data per case.
Why “Sealed”?
The word sealed describes the key restriction: all subtypes must be defined in the same file as the sealed class itself. No one outside this file can add a new subtype.
That’s what makes exhaustive when work. Kotlin can look at the whole file, see exactly which subtypes exist, and verify your when handles all of them. If new subtypes could appear later in some other file, the compiler couldn’t make that guarantee.
In real Android code, sealed classes are the standard way to model UI state. A screen might have a
sealed class UiState { object Loading; data class Content(...); data class Empty(val message); data class Error(val cause) }. The view layer pattern-matches on the current state and renders accordingly. It’s clean, type-safe, and impossible to forget a case.
Sealed Classes vs Enums Side-by-Side
A quick comparison to lock the distinction in:
Enum
Sealed class
Fixed set of choices?
✅ Yes
✅ Yes
Each value can carry different data?
❌ All values share the same shape
✅ Each subtype has its own fields
Multiple instances per “value”?
❌ Each value is a single instance
✅ Subtypes (especially data classes) can have many instances with different data
Exhaustive when?
✅ Yes
✅ Yes
Good for…
Pure labels: directions, log levels, days of the week
Variant data: states, results, events
The cleanest mental model:
Enum = a list of labels. Sealed class = a list of shapes, where each shape carries its own data.
If you only need labels, use enum (less ceremony). If each case carries different data, use sealed class.
A Real Example: A Tiny State Machine
Let’s tie it all together with something concrete. We’ll model a simple traffic light using an enum, plus a UI state using a sealed class:
enum class LightColor(val durationSeconds: Int) {
RED(30),
YELLOW(5),
GREEN(25);
fun next(): LightColor = when (this) {
RED -> GREEN
GREEN -> YELLOW
YELLOW -> RED
}
}
sealed class TrafficScreenState {
object Loading : TrafficScreenState()
data class Showing(val light: LightColor) : TrafficScreenState()
data class Error(val reason: String) : TrafficScreenState()
}
fun render(state: TrafficScreenState) {
when (state) {
is TrafficScreenState.Loading -> println("⏳ Loading traffic light status...")
is TrafficScreenState.Showing -> println("🚦 Current: ${state.light} (${state.light.durationSeconds}s)")
is TrafficScreenState.Error -> println("⚠️ Couldn't fetch: ${state.reason}")
}
}
fun main() {
val states = listOf(
TrafficScreenState.Loading,
TrafficScreenState.Showing(LightColor.GREEN),
TrafficScreenState.Error("No network"),
TrafficScreenState.Showing(LightColor.GREEN.next())
)
states.forEach { render(it) }
}
Output:
⏳ Loading traffic light status...
🚦 Current: GREEN (25s)
⚠️ Couldn't fetch: No network
🚦 Current: YELLOW (5s)
In ~30 lines you’ve modeled:
- A fixed set of traffic light colors with their durations (enum)
- A finite set of UI screen states, each carrying its own data (sealed class)
- A rendering function that handles every possible state safely (exhaustive
when)
This shape — enum + sealed class + when — is the modern Kotlin/Android way to model “things that can be in one of several specific states.” It’s also the foundation of how modern UI frameworks (like Jetpack Compose, which we cover in Phase 3) think about screens.
Common Mistakes Beginners Make
❌ Mistake 1: Using strings for fixed choices
fun setStatus(status: String) { // ❌ "Active"? "active"? "ACT"? "banana"?
if (status == "active") { ... }
}
enum class Status { ACTIVE, PAUSED, ARCHIVED }
fun setStatus(status: Status) { // ✅ Only these three are allowed
}
❌ Mistake 2: Forgetting the semicolon after enum values when adding methods
enum class Color {
RED, GREEN, BLUE
fun hex(): String = ... // ❌ Missing semicolon after BLUE
RED, GREEN, BLUE;
fun hex(): String = ... // ✅
}
❌ Mistake 3: Treating sealed classes like enums (forgetting is)
when (state) {
NetworkResult.Loading -> ... // ⚠️ Works for the `object`...
NetworkResult.Success -> ... // ❌ Error — Success is a data class, not a single instance
is NetworkResult.Success -> ... // ✅ Use `is` for data class subtypes
}
Use is with sealed class subtypes that have data; you can omit is only for object subtypes (which are singletons).
❌ Mistake 4: Putting sealed subtypes in a separate file
// File1.kt
sealed class Animal
// File2.kt
class Dog : Animal() // ⚠️ Used to be illegal; modern Kotlin allows this
// but only within the same MODULE
Modern Kotlin (1.5+) loosened this — subtypes can be in the same module now, not strictly the same file. For your purposes, keep them in the same file. It’s simpler and matches what most tutorials show.
❌ Mistake 5: Forgetting that enums and sealed classes both make when exhaustive
fun handle(d: Direction) {
when (d) {
Direction.NORTH -> ...
Direction.SOUTH -> ...
// ⚠️ Missing EAST and WEST
// Kotlin won't compile if this is an expression (val x = when (d) { ... })
// But as a STATEMENT (no return value used), it'll silently compile.
}
}
To get the full exhaustive-checking benefit, use when as an expression (assign its result to a variable, or use it as a return value). Statement-form when is more lenient.
💪 Today’s Practice Challenge
Build a small task tracker combining everything.
enum class Priority(val label: String, val sortOrder: Int) {
HIGH("🔴 High", 0),
MEDIUM("🟡 Medium", 1),
LOW("🟢 Low", 2);
}
sealed class TaskState {
object Pending : TaskState()
data class InProgress(val percentComplete: Int) : TaskState()
object Done : TaskState()
data class Blocked(val reason: String) : TaskState()
}
data class Task(
val title: String,
val priority: Priority,
val state: TaskState
)
fun summarize(task: Task): String {
// Your code:
// Use a when on task.state to produce a one-line summary like:
// "[🔴 High] Submit report — In Progress (40%)"
// "[🟢 Low] Buy groceries — Pending"
// "[🟡 Medium] Fix bug #42 — Blocked (Waiting for design review)"
// "[🔴 High] Write blog post — Done ✅"
return ""
}
fun main() {
val tasks = listOf(
Task("Submit report", Priority.HIGH, TaskState.InProgress(40)),
Task("Buy groceries", Priority.LOW, TaskState.Pending),
Task("Fix bug #42", Priority.MEDIUM, TaskState.Blocked("Waiting for design review")),
Task("Write blog post", Priority.HIGH, TaskState.Done)
)
tasks.forEach { println(summarize(it)) }
}
Bonus challenges:
- Sort the tasks by priority before printing (hint:
sortedBy { it.priority.sortOrder }) - Use Day 15 collection operations: count how many tasks are
Done,Pending,Blocked. You’ll needcount { it.state is TaskState.Done }etc. - Add a method to
PrioritycalledisUrgent(): Booleanthat returnstrueforHIGHonly - Stretch: Add a fourth
TaskState—Cancelled(val cancelledBy: String). Notice how the compiler immediately complains about everywhenblock that handledTaskState— and you go fix them all. That’s exhaustivewhensaving you from forgotten cases.
🎓 Practice Beyond This Article
The **Kotlin docs on sealed classes** have several short examples worth reading. They’re the closest official source for “this is the modern way to model variant data in Kotlin.”
👉 Tomorrow: Day 23
For 22 days you’ve built up Kotlin step by step, designed for absolute beginners. Tomorrow’s article is different — it’s a bridge article for developers who already know some other language (JavaScript, Python, Java, Dart) and want to fast-track their way into Kotlin without slogging through every previous day. It compresses Phase 1 into one focused article. If you’re an absolute beginner, you can skip it — it’s not for you. If you came from another language, it’ll be the article you wish you’d had on Day 1.
See you tomorrow.
📚 Series Navigation
Previous: Day 21 — Data Classes (link) You are here: Day 22 — Enums & Sealed Classes Next: Day 23 — Kotlin Fast-Track for Existing Developers (link coming tomorrow)
If this article helped, a clap or share means the world. It tells Medium to surface this series to more learners.
📊 References & Further Reading
메타데이터
- post_id
- 3353f22a1f2d
- slug
- day-22-enums-sealed-classes-modeling-one-of-these-things-3353f22a1f2d
- url
- https://medium.com/@kirupakaran094/day-22-enums-sealed-classes-modeling-one-of-these-things-3353f22a1f2d
- canonical_url
- https://medium.com/@kirupakaran094/day-22-enums-sealed-classes-modeling-one-of-these-things-3353f22a1f2d
- author_url
- https://medium.com/@kirupakaran094
- status
- ok
- fetched_at
- 2026-06-16 19:09:56