Kotlin Enums and Sealed Classes

Model a fixed set of options with enum classes and a fixed set of related types with sealed classes and interfaces, then handle every case with an exhaustive when.

Lesson 17 of 43Intermediate18 min read

Many values in a program come from a small, fixed set of options. A T-shirt is small, medium or large. A traffic light is red, amber or green. Representing them as strings like "medium" invites typos, and nothing stops "medum" slipping through. An enum class defines a type whose only possible values are the ones you list, so typos become compile errors.

Sometimes the options aren't simple labels but different shapes of data. A network request is either loading, succeeded with some data, or failed with an error message. Each case carries different information. Sealed classes model exactly that: a closed family of types where the compiler knows every member.

Both pair beautifully with when. Because the compiler knows every possible case, a when over an enum or sealed type can be exhaustive without an else, and if you add a new case later, the compiler points at every place that needs updating.

Enum classes

Kotlin
enum class Size { SMALL, MEDIUM, LARGE }

val mine = Size.MEDIUM
println(mine)               // MEDIUM
println(mine.ordinal)       // 1
println(Size.entries)       // [SMALL, MEDIUM, LARGE]
  • Constants are written in capitals, separated by commas.
  • name is the constant's name as a String; ordinal is its position from 0.
  • Size.entries lists every constant (Kotlin 1.9+; older code uses Size.values()).
  • Size.valueOf("LARGE") converts text to a constant and throws if there's no match. For safe lookups, use Size.entries.find { it.name == text }.

Enums with properties and methods

Enum constants can carry data and behaviour. Add a constructor, and end the list of constants with a semicolon before any members:

Kotlin
enum class Coffee(val label: String, val price: Double) {
    ESPRESSO("Espresso", 2.20),
    LATTE("Latte", 3.10),
    FLAT_WHITE("Flat white", 3.30);

    fun priceFor(cups: Int) = price * cups
}

Each constant passes its own values to the constructor. This keeps related data together and avoids parallel lists or maps that can fall out of step.

Sealed classes and interfaces

A sealed class or interface allows only the subclasses declared in the same package and module, so the compiler knows the complete list:

Kotlin
sealed interface LoadState {
    data object Loading : LoadState
    data class Success(val items: List<String>) : LoadState
    data class Error(val message: String) : LoadState
}
  • Each case can be a data class with its own properties, or a data object when it carries no data.
  • Unlike enum constants, each case can have a different shape.
  • Sealed interfaces are usually preferred today, because a class can implement several interfaces.

Exhaustive when

Kotlin
fun render(state: LoadState): String = when (state) {
    LoadState.Loading -> "Loading..."
    is LoadState.Success -> "${state.items.size} items"
    is LoadState.Error -> "Error: ${state.message}"
}
  • No else is needed, because every case is covered.
  • is checks smart cast the value, so state.items and state.message are available.
  • If someone adds a fourth case, every when like this stops compiling until the new case is handled. Avoid adding else to these, or you lose that protection.

Ordering enums and giving each constant its own code

Enum constants are ordered by the position you declared them in, so you can compare them with <, > and ranges. Declaring sizes from smallest to largest pays off here:

Kotlin
enum class Size { SMALL, MEDIUM, LARGE }

println(Size.LARGE > Size.SMALL)                       // true
println(Size.entries.filter { it >= Size.MEDIUM })     // [MEDIUM, LARGE]

Because the order comes from the declaration, inserting a new constant in the middle changes every ordinal after it. That's fine inside a running program, but never save ordinal to a file or database; save name instead, which stays the same.

Sometimes each constant needs different behaviour, not just different data. Declare an abstract function in the enum and give every constant its own body:

Kotlin
enum class Op(val symbol: String) {
    PLUS("+") { override fun apply(a: Int, b: Int) = a + b },
    TIMES("*") { override fun apply(a: Int, b: Int) = a * b };

    abstract fun apply(a: Int, b: Int): Int
}

for (op in Op.entries) println("6 ${op.symbol} 7 = ${op.apply(6, 7)}")
// 6 + 7 = 13
// 6 * 7 = 42

The compiler insists that every constant implements apply, so a new operation can't be added half-finished. Enum functions can also use entries to move between constants, which suits cycles such as traffic lights:

Kotlin
enum class Light { RED, AMBER, GREEN;
    fun next() = entries[(ordinal + 1) % entries.size]
}
println(Light.GREEN.next())    // RED

Kotlin Enums and Sealed Classes Syntax

Enums and sealed types

enum class Light { RED, AMBER, GREEN }
enum class Planet(val g: Double) { EARTH(9.8), MARS(3.7) }
Light.entries   Light.valueOf("RED")   light.name   light.ordinal

sealed interface Result {
    data class Ok(val value: Int) : Result
    data class Fail(val reason: String) : Result
    data object Pending : Result
}
when (r) {
    is Result.Ok -> ...
    is Result.Fail -> ...
    Result.Pending -> ...
}

Kotlin Enums and Sealed Classes Summary

  • Enum classes define a fixed set of named constants; use name, ordinal, entries and valueOf.
  • Enum constants can have properties and methods via a constructor.
  • Sealed classes and interfaces define a closed family of types with different data.
  • data object suits cases without data; data class suits cases with data.
  • A when over an enum or sealed type is exhaustive without else, and the compiler flags missing cases.

Kotlin Enums and Sealed Classes Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Traffic lights with an enum

Kotlin
enum class Light(val seconds: Int) {
    RED(30), AMBER(3), GREEN(25);

    fun next(): Light = when (this) {
        RED -> GREEN
        GREEN -> AMBER
        AMBER -> RED
    }
}

fun main() {
    var light = Light.RED
    repeat(4) {
        println("${light.name} for ${light.seconds}s")
        light = light.next()
    }
    println(Light.entries.sumOf { it.seconds })
}
Output
RED for 30s
GREEN for 25s
AMBER for 3s
RED for 30s
58
Line-by-line explanation
  1. Lines 1 to 2Each constant passes its duration to the constructor; the semicolon ends the list.
  2. Lines 4 to 8Inside the enum, when over this covers every constant, so no else is needed.
  3. Line 17entries lists every light, and sumOf adds their durations.
Example 2

Screen states with a sealed interface

Kotlin
sealed interface UiState {
    data object Loading : UiState
    data class Content(val items: List<String>) : UiState
    data class Error(val message: String, val canRetry: Boolean) : UiState
}

fun render(state: UiState): String = when (state) {
    UiState.Loading -> "Spinner"
    is UiState.Content -> if (state.items.isEmpty()) "Nothing here yet" else state.items.joinToString()
    is UiState.Error -> "${state.message}" + if (state.canRetry) " [Retry]" else ""
}

fun main() {
    val states = listOf(
        UiState.Loading,
        UiState.Content(listOf("Tea", "Cake")),
        UiState.Content(emptyList()),
        UiState.Error("No connection", canRetry = true)
    )
    states.forEach { println(render(it)) }
}
Output
Spinner
Tea, Cake
Nothing here yet
No connection [Retry]
Line-by-line explanation
  1. Line 2A data object for the case with no data.
  2. Lines 3 to 4Each case carries exactly the data it needs.
  3. Lines 7 to 11An exhaustive when: every case is handled, and smart casts give access to each case's properties.
Example 3

Safe lookups and results

Kotlin
enum class Size { SMALL, MEDIUM, LARGE }

sealed interface Parse {
    data class Ok(val size: Size) : Parse
    data class Bad(val input: String) : Parse
}

fun parseSize(text: String): Parse =
    Size.entries.find { it.name.equals(text.trim(), ignoreCase = true) }
        ?.let { Parse.Ok(it) }
        ?: Parse.Bad(text)

fun main() {
    for (input in listOf("medium", " LARGE ", "huge")) {
        when (val r = parseSize(input)) {
            is Parse.Ok -> println("Got ${r.size} (position ${r.size.ordinal})")
            is Parse.Bad -> println("'${r.input}' isn't a size")
        }
    }
}
Output
Got MEDIUM (position 1)
Got LARGE (position 2)
'huge' isn't a size
Line-by-line explanation
  1. Lines 8 to 11find looks for a matching constant ignoring case; let wraps it in Ok, and Elvis returns Bad when nothing matched.
  2. Line 15when (val r = ...) declares a variable scoped to the when.
  3. Lines 16 to 17Both cases handled, so no else is needed.

Common Kotlin Enums and Sealed Classes Mistakes

Strings instead of an enum

Don't
var size = "medum"              // typo compiles fine
Do
var size = Size.MEDIUM          // a typo would be a compile error

valueOf with user input

Don't
val size = Size.valueOf(input)  // IllegalArgumentException for "large" or "huge"
Do
val size = Size.entries.find { it.name.equals(input, ignoreCase = true) } ?: Size.MEDIUM

else in a sealed when

Don't
when (state) {
    is UiState.Content -> ...
    else -> ...                 // a new state added later is silently treated as else
}
Do
when (state) {
    UiState.Loading -> ...
    is UiState.Content -> ...
    is UiState.Error -> ...
}

Kotlin Enums and Sealed Classes in Real Projects

Enums and sealed types in practice

Modern Android apps describe every screen's state with a sealed interface, and the UI uses an exhaustive when to show the right thing. Network and database layers return sealed Result-style types instead of throwing, so callers must handle failure. Enums represent things like sort orders, user roles and payment methods, often with a label property for display.

Kotlin Enums and Sealed Classes FAQ

How do I convert a string to an enum in Kotlin?

Size.valueOf("LARGE") works when the text exactly matches a constant's name, but throws an IllegalArgumentException otherwise. For input from users or files, use Size.entries.find { it.name.equals(text, ignoreCase = true) }, which returns null when nothing matches so you can supply a default with ?:.

What is the difference between a sealed class and an abstract class?

Anyone can subclass an abstract class, anywhere, so the compiler never knows the full list of subclasses. A sealed class only allows subclasses in the same package and module, which is why a when over it can be checked for exhaustiveness without an else. Both can hold shared code and abstract members.

Can an enum class implement an interface in Kotlin?

Yes. Write enum class Coffee : Priced { ... } and implement the interface's members, either once for the whole enum or separately in each constant's body. An enum can't extend another class, though, because every enum already extends Enum.

Do the subclasses of a sealed class have to be in the same file?

Not any more. Since Kotlin 1.5, subclasses can live in different files, as long as they're in the same package and the same module as the sealed class. Many teams still keep them together, often nested inside the sealed type, so the complete list is easy to read.

How do I loop over all values of an enum?

Use entries, a list of every constant in declaration order: for (s in Size.entries) println(s). Because it's a normal list, you can also filter it, map it or pick a random constant with Size.entries.random(). Older code uses Size.values(), which returns a new array each time it's called.

Kotlin Enums and Sealed Classes Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the declaration of an enum for directions.

 class Direction { NORTH, EAST, SOUTH, WEST }
Predict the outputeasy

What does this print?

enum class Size { S, M, L }
println(Size.L.ordinal)
Multiple choicemedium

When should you use a sealed class instead of an enum?

Multiple choicemedium

Why can a when over a sealed interface skip else?

Predict the outputhard

What does this print?

enum class Op(val f: (Int, Int) -> Int) {
    ADD({ a, b -> a + b }), MUL({ a, b -> a * b })
}
println(Op.entries.map { it.f(3, 4) })

Kotlin Enums and Sealed Classes Quiz

5 questions to check what you learned.

  1. What does Size.valueOf("HUGE") do if HUGE isn't a constant?

    • Returns null
    • Returns the first constant
    • Throws an IllegalArgumentException
    • Creates HUGE
  2. What does Size.entries return?

    • A list of every constant in declaration order
    • The number of constants
    • The names as Strings
    • A map
  3. Which is best for a case with no data in a sealed hierarchy?

    • data class Loading()
    • enum Loading
    • val Loading
    • data object Loading
  4. Can enum constants have properties?

    • No
    • Only String properties
    • Yes, through the enum's constructor
    • Only in sealed classes
  5. What happens when you add a new case to a sealed interface?

    • Nothing
    • Every exhaustive when without else stops compiling until the new case is handled
    • Existing objects are deleted
    • The new case is ignored