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
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.
nameis the constant's name as a String;ordinalis its position from 0.Size.entrieslists every constant (Kotlin 1.9+; older code usesSize.values()).Size.valueOf("LARGE")converts text to a constant and throws if there's no match. For safe lookups, useSize.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:
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:
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 classwith its own properties, or adata objectwhen 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
fun render(state: LoadState): String = when (state) {
LoadState.Loading -> "Loading..."
is LoadState.Success -> "${state.items.size} items"
is LoadState.Error -> "Error: ${state.message}"
}- No
elseis needed, because every case is covered. ischecks smart cast the value, sostate.itemsandstate.messageare available.- If someone adds a fourth case, every
whenlike this stops compiling until the new case is handled. Avoid addingelseto 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:
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:
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 = 42The 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:
enum class Light { RED, AMBER, GREEN;
fun next() = entries[(ordinal + 1) % entries.size]
}
println(Light.GREEN.next()) // REDKotlin 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.
Traffic lights with an enum
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 })
}RED for 30s
GREEN for 25s
AMBER for 3s
RED for 30s
58Line-by-line explanation
- Lines 1 to 2Each constant passes its duration to the constructor; the semicolon ends the list.
- Lines 4 to 8Inside the enum, when over this covers every constant, so no else is needed.
- Line 17entries lists every light, and sumOf adds their durations.
Screen states with a sealed interface
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)) }
}Spinner
Tea, Cake
Nothing here yet
No connection [Retry]Line-by-line explanation
- Line 2A data object for the case with no data.
- Lines 3 to 4Each case carries exactly the data it needs.
- Lines 7 to 11An exhaustive when: every case is handled, and smart casts give access to each case's properties.
Safe lookups and results
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")
}
}
}Got MEDIUM (position 1)
Got LARGE (position 2)
'huge' isn't a sizeLine-by-line explanation
- Lines 8 to 11find looks for a matching constant ignoring case; let wraps it in Ok, and Elvis returns Bad when nothing matched.
- Line 15when (val r = ...) declares a variable scoped to the when.
- Lines 16 to 17Both cases handled, so no else is needed.
Common Kotlin Enums and Sealed Classes Mistakes
Strings instead of an enum
var size = "medum" // typo compiles finevar size = Size.MEDIUM // a typo would be a compile errorvalueOf with user input
val size = Size.valueOf(input) // IllegalArgumentException for "large" or "huge"val size = Size.entries.find { it.name.equals(input, ignoreCase = true) } ?: Size.MEDIUMelse in a sealed when
when (state) {
is UiState.Content -> ...
else -> ... // a new state added later is silently treated as else
}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.
Complete the declaration of an enum for directions.
class Direction { NORTH, EAST, SOUTH, WEST }
What does this print?
enum class Size { S, M, L }
println(Size.L.ordinal)
When should you use a sealed class instead of an enum?
Why can a when over a sealed interface skip else?
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.
What does
Size.valueOf("HUGE")do if HUGE isn't a constant?- Returns null
- Returns the first constant
- Throws an IllegalArgumentException
- Creates HUGE
What does
Size.entriesreturn?- A list of every constant in declaration order
- The number of constants
- The names as Strings
- A map
Which is best for a case with no data in a sealed hierarchy?
- data class Loading()
- enum Loading
- val Loading
- data object Loading
Can enum constants have properties?
- No
- Only String properties
- Yes, through the enum's constructor
- Only in sealed classes
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