Enums and sealed classes
Some things can only be one of a few options. Enums and sealed classes let you say so — and then the compiler refuses to build your app until you have handled every single one.
The typo that compiles
A focus timer is either running a work session or running a break. Written with what you know so far, that might be:
1var phase = "focus"
2
3if (phase == "brake") {
4 println("Time for tea")
5}Read the third line again. brake, not break. This compiles. It runs. It never prints anything, ever, and there is no error message to help you — because as far as Kotlin is concerned, "brake" is a perfectly good piece of text that simply never matches.
are terrible at describing choices. There are billions of possible strings and only two of them mean anything to you. The other billions are all typos waiting to happen.
This lesson is about making the your proofreader instead.
A traffic light has exactly three lamps: red, amber, green. You cannot install a purple one. The set is fixed, and everyone who deals with a traffic light knows the complete list.
That is an enum.
Now think about tickets at the same station. There is a child ticket, an adult ticket, and a group ticket — and a group ticket carries an extra fact the others do not have: how many people. Still a fixed, known list of kinds. But each kind carries different information.
That is a sealed class.
The valuable part of both is the same: because the list is closed, an inspector can check that you have thought about every case.
Enums: a small, fixed set of names
enum class Phase { FOCUS, BREAK }That declares a brand-new . A value of type Phase can be Phase.FOCUS or Phase.BREAK, and nothing else in the universe. Writing Phase.BRAKE does not compile — the typo that survived above is now caught in the editor.
By convention the names are written in capitals. Each one is an that already exists; you never build them.
1val p = Phase.FOCUS
2println(p)
3println(p.name)
4println(Phase.entries)1FOCUS
2FOCUS
3[FOCUS, BREAK]entries gives you the complete list in declared order, which is perfect for a for loop. (In code written before 2023 you will see Phase.values() doing the same job.)
Enums can carry data and behaviour
An enum constant is allowed a , just like any other class:
1enum class Phase(val minutes: Int) {
2 FOCUS(25),
3 BREAK(5)
4}Now Phase.FOCUS.minutes is 25. The lengths live with the phases they describe instead of being scattered through your code as loose numbers.
The payoff: when becomes exhaustive
Here is why any of this matters.
1fun advice(p: Phase): String = when (p) {
2 Phase.FOCUS -> "Head down."
3 Phase.BREAK -> "Stand up and stretch."
4}Look at what is missing: there is no else. With a over a you would need one, because there is always another possible string. Over an enum there is not — you have covered the entire list, so Kotlin accepts it. That is called being .
Now suppose the app grows and you add a third phase:
enum class Phase { FOCUS, BREAK, LONG_BREAK }The build stops immediately:
1e: 'when' expression must be exhaustive, add necessary
2'LONG_BREAK' branch or 'else' branch insteadRead that as good news. You changed one thing in one file, and the compiler walked your entire project and pointed at every single place that now needs a decision. Nothing is silently wrong. Nothing gets forgotten. This is the closest thing programming has to a safety net.
An else branch throws that safety net away. Once a when has else, adding a new option compiles quietly and falls into the catch-all — which is exactly the bug the enum was meant to prevent.
Over an enum or a sealed class, prefer listing every case. Save else for when over numbers and text, where the list genuinely is endless.
Sealed classes: when each option carries different data
Enums are perfect when the options are just names. They fall apart the moment one option needs to carry something the others do not.
Think about a screen loading a list of notes. It is in exactly one of three states:
- Loading — nothing to say yet.
- Ready — and here are the notes.
- Failed — and here is what went wrong.
Only two of those carry data, and they carry different data. An enum cannot express that. A can:
1sealed class Screen {
2 object Loading : Screen()
3 data class Ready(val count: Int) : Screen()
4 data class Failed(val why: String) : Screen()
5}The word sealed means: this is the complete list of possible kinds, and no one can add another from outside this file. That closed list is what lets the compiler check your work.
Each entry is a — the : Screen() after the name says "this counts as a Screen too".
object Loading is new. It means there is exactly one Loading and it holds no information, so you never build one — you just write Screen.Loading. Ready and Failed are ordinary , built with brackets like anything else: Screen.Ready(3).
Checking which one you have
1fun describe(s: Screen): String = when (s) {
2 Screen.Loading -> "Loading..."
3 is Screen.Ready -> "Ready: ${s.count} notes"
4 is Screen.Failed -> "Failed: ${s.why}"
5}is asks "is it this kind?". And inside that branch, something excellent happens: s.count just works. The compiler followed the check and now knows exactly what s is — the same that made if (name != null) work in Lesson 1.13.
Screen.Loading needs no is, because there is only one of it, so a plain equality check is enough.
And again there is no else. Add a fourth state — say object Empty : Screen() — and every when over a Screen in your whole project stops compiling until you say what an empty screen should show. Part 6 has a whole lesson on empty and error states, and this is the mechanism that stops you shipping without them.
In real Android code you will more often see sealed interface than sealed class. For everything in this course the two behave the same way — closed list, exhaustive when. Part 4 uses one for the state of a whole screen.
Which to use
| Situation | Use |
|---|---|
| A fixed list of plain names | enum class |
| A fixed list where the options carry different data | sealed class |
| An open-ended value like a name or a message | Plain String |
- Open Pocket Studio, tap Projects, and open your Kotlin practice project.
- Tap Editor and open
Playground.kt. - Select everything in the file and delete it.
- Type the program from the walkthrough above.
- Tap Run and read the Output panel.
You should see exactly this:
1FOCUS lasts 25 min
2BREAK lasts 5 min
3Loading...
4Ready: 3 notes
5Failed: no disk- Now feel the safety net. Add one line inside the
Screenbraces:object Empty : Screen(). - Tap Run. The build fails before anything happens, with 'when' expression must be exhaustive, add necessary 'Empty' branch or 'else' branch instead.
- Fix it properly by adding
Screen.Empty -> "Nothing here yet"to thewhen, then run again. Five lines of output as before — and now a state you cannot forget about.
when is being used as a value — its result is returned or assigned — and one of the possible cases has no branch. The message names exactly which one.else only if the missing cases genuinely all deserve the same answer, because else switches this check off for good.Screen(). The sealed class itself is only a label for the family — the real things are its subclasses.Screen.Ready(3), or just refer to Screen.Loading.Screen.Loading() with brackets. Loading was declared with object, so exactly one of it already exists and there is nothing to construct.Screen.Loading.s.count outside an is Screen.Ready branch. At that point the compiler only knows s is some kind of Screen, and not every Screen has a count.Phase.FOCUS. Type Phase. in the editor and Pocket Studio will list every valid constant.- An declares a fixed list of named options. Constants can carry values and
entrieslists them all. - A declares a fixed list of kinds, where each kind can carry different data.
objectfor the ones with nothing to carry,data classfor the rest. - Over either of them, a with a branch for every case is — no
elserequired, and no case forgotten. - Adding a new case breaks the build everywhere it now needs handling. That is the feature, not a nuisance.
ismatches a kind and gives you a inside the branch.- Next: lambdas — what those braces you have been handing to
filterreally are, and how to write functions that take other functions.