← Back to list

Day 22: Enums & Sealed Classes — Modeling “One Of These Things”

Android with Kotlin for Absolute Beginners — Day 22 of the Series

Kirubakaran · 2026-06-03 01:41 · 0 claps · 10.3 min read
#enum #kotlin #android-app-development #programming #mobile-app-development
Open on Medium ↗
Wiki topics: 💻 · Programming 📱 · Mobile Development

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 Direction declares a new type
  • NORTH, SOUTH, EAST, WEST are the only values a Direction can 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 .name property (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 when block that handled Direction will 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, this refers to whichever enum value the method is being called on
  • The when inside opposite() doesn’t need else because every value of Direction is 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.entries instead of values() — it returns a List instead of an Array, which integrates better with collection operations like map and filter. Either works; entries is 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. The sealed keyword means “the only subtypes of this class are defined right here, in this file.”
  • **object Loading** — an object is a “single instance” — there’s only ever one Loading, since it carries no data. It’s Kotlin’s built-in way to express a singleton. We’ve informally seen object before; 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:

  1. **is NetworkResult.Loading** — the is operator checks the type. (You met is briefly in Day 8’s smart casts.)
  2. Inside each branch, Kotlin “smart casts” result — in the Success branch, you can access result.data directly without any explicit casting, because Kotlin knows from the is check that result is specifically a Success there. Same for result.message in the Error branch.
  3. No else needed. Sealed classes give you the same exhaustive when superpower 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 need count { it.state is TaskState.Done } etc.
  • Add a method to Priority called isUrgent(): Boolean that returns true for HIGH only
  • Stretch: Add a fourth TaskState — Cancelled(val cancelledBy: String). Notice how the compiler immediately complains about every when block that handled TaskState — and you go fix them all. That’s exhaustive when saving 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