Chapters

Hide chapters

Kotlin Apprentice

Third Edition · Android 11 · Kotlin 1.4 · IntelliJ IDEA 2020.3

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

16. Enum & Sealed Classes
Written by Ellen Shapiro

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Sometimes you’ll have a piece of information that could (or at least, should) have only one of a limited number of potential values. Using what you already know, you could make a List of all the acceptable values for that piece of information, and walk (or enumerate) through each value, one-by-one, to see if your new piece of information matches one of the expected values.

If you think that sounds boring and repetitive, you’re not alone. This is why the concept of the enum was invented.

Note: There is some debate over how to pronounce the word enum. Since it derives from “enumeration,” some people pronounce it ee-noom. Some people pronounce it ee-numb, since in its shortened form, it looks a lot more like the prefix to the word “number.”

This book takes no position on which of these is the preferred pronunciation, but you should note that both pronunciations are used commonly, and people tend to feel quite strongly about which pronunciation is the “correct” one. Caveat coder.

In Kotlin, as in many other programming languages, an enum is its own specialized type, indicating that something has a number of possible values.

One big difference in Kotlin is that enums are made by creating an enum class. You get a number of interesting pieces of functionality that enums in other languages don’t necessarily have. As you work through this chapter, you’ll learn about some of the most commonly-used bits of functionality and how to take advantage of them as you work in Kotlin.

To get started, open the starter project for this chapter and dig in.

Creating your first enum class

Open up main.kt. Above the main() function, define a new enum class:

enum class DayOfTheWeek {
  // more code goes here
}

Next, replace the comment by adding a comma-separated list of cases, or individual values, for the day of the week:

Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday

In the main() function, replace the existing println() statement with one which goes through and prints out some information you get for free with any enum class:

for (day in DayOfTheWeek.values()) {
  println("Day ${day.ordinal}: ${day.name}")
}

Run the updated main() function, and it should print out the following:

Day 0: Sunday
Day 1: Monday
Day 2: Tuesday
Day 3: Wednesday
Day 4: Thursday
Day 5: Friday
Day 6: Saturday

Neat! So what did you just get for free from Kotlin by declaring DaysOfTheWeek to be an enum class?

  • The values() companion function on the enum class gives you a List of all the declared cases in the class, making it easy to go through all possibilities, and also to find out how many possibilities exist.
  • The ordinal property of each case gives that case’s index in the list of declared cases. You’ll note from what’s printed out that the order is zero-indexed.
  • The name property of each case takes the name of the case in code and gives back the String value of that name.

A lot of this behavior is possible because enum classes are, well, classes. Each case is an instance of the class, so things like compiler-generated companion object functions for the class itself and individual properties for each instance are possible. Additionally, because these properties return objects of their own, you can use other functionality like getting the day based on a passed-in integer index.

For example, let’s say your colleagues working somewhere else in the code tell you that they’ll hand you an integer representing the day of the week. You could use the functionality of List, with which you’re already familiar, to get the value at the appropriate index.

Add the following to the main() function to see this in action:

val dayIndex = 0
val dayAtIndex = DayOfTheWeek.values()[dayIndex]
println("Day at $dayIndex is $dayAtIndex")

Run the main() function again, and at the end, you’ll see:

Day at 0 is Sunday

If you want, you can even change the index of the day to update the value returned. Make sure not to go beyond the length of values(), as that will throw an ArrayIndexOutOfBoundsException, just like it will with any other list in Kotlin.

Another nice piece of functionality you get for free is the valueOf() method, which takes a String and returns the enum instance matching that string.

Add the following to the bottom of the main() function:

val tuesday = DayOfTheWeek.valueOf("Tuesday")
println("Tuesday is day ${tuesday.ordinal}")

Run the main() function, and at the end of the output you’ll see:

Tuesday is day 2

Neat! Now, the eagle-eyed among you may have noticed that the valueOf() function doesn’t return a nullable. So what happens when you try to get the value of an enum case that doesn’t exist? Let’s find out.

Add the following lines to the main() function:

val notADay = DayOfTheWeek.valueOf("Blernsday")
println("Not a day: $notADay")

Run main() again, and:

Exception in thread "main" java.lang.IllegalArgumentException: No enum constant DayOfTheWeek.Blernsday
	at java.lang.Enum.valueOf(Enum.java:238)
	at DayOfTheWeek.valueOf(main.kt)
    at MainKt.main(main.kt:23)

Nooooo! Weren’t Kotlin’s nullables supposed to save us from these “thing doesn’t exist” exceptions!?

The designers of Kotlin decided that trying to access an enum case which doesn’t exist, akin to accessing an index outside the bounds of an array, was enough of an error that an exception should be thrown. So that stopped your process dead in its tracks.

Delete the last two lines you added looking for “Blernsday” so the rest of your code runs.

Updating case order

Another nice thing about enum classes is that if you find out something needs to be in a different order from a zero-indexed perspective, it’s easy to make that change.

Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday;
Day 0: Monday
Day 1: Tuesday
Day 2: Wednesday
Day 3: Thursday
Day 4: Friday
Day 5: Saturday
Day 6: Sunday
Day at 0 is Monday
Tuesday is day 1

Enum class properties and functions

Like other classes, enum classes can have properties and functions. You can even set them up to be passed in as part of the constructor for each case.

enum class DayOfTheWeek(val isWeekend: Boolean)
Monday(false),
Tuesday(false),
Wednesday(false),
Thursday(false),
Friday(false),
Saturday(true),
Sunday(true);
println(
  "Day ${day.ordinal}: ${day.name}, is weekend: ${day.isWeekend}"
)
Day 0: Monday, is weekend: false
Day 1: Tuesday, is weekend: false
Day 2: Wednesday, is weekend: false
Day 3: Thursday, is weekend: false
Day 4: Friday, is weekend: false
Day 5: Saturday, is weekend: true
Day 6: Sunday, is weekend: true
enum class DayOfTheWeek(val isWeekend: Boolean = false)
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday(true),
Sunday(true);
companion object {
  fun today(): DayOfTheWeek {
    // Code goes here
  }
}
// 1
val calendarDayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
// 2
var adjustedDay = calendarDayOfWeek - 2
// 3
val days = DayOfTheWeek.values()
if (adjustedDay < 0) {
  adjustedDay += days.count()
}
// 4
val today = days.first { it.ordinal == adjustedDay }
return today
val today = DayOfTheWeek.today()
val isWeekend =
  "It is${if (today.isWeekend) "" else " not"} the weekend"
println("It is $today. $isWeekend.")
It is Monday. It is not the weekend.
fun daysUntil(other: DayOfTheWeek): Int {
  // 1
  return if (this.ordinal < other.ordinal) {
    // 2
    other.ordinal - this.ordinal
  } else {
    //3
    other.ordinal - this.ordinal + DayOfTheWeek.values().count()
  }
}
val secondDay = DayOfTheWeek.Friday
val daysUntil = today.daysUntil(secondDay)
println("It is $today. $isWeekend. There are $daysUntil days until $secondDay.")
It is Monday. It is not the weekend. There are 4 days until Friday.

Using when with enum classes

One of the most powerful features of enum classes is how they combine with the when expression. You’ve already seen how this can be used on basic types like Int and String.

when (today) {
  DayOfTheWeek.Monday -> println("I don't care if $today's blue")
  DayOfTheWeek.Tuesday -> println("$today's gray")
  DayOfTheWeek.Wednesday -> println("And $today, too")
  DayOfTheWeek.Thursday -> println("$today, I don't care 'bout you")
  DayOfTheWeek.Friday -> println("It's $today, I'm in love")
  DayOfTheWeek.Saturday -> println("$today, Wait...")
  DayOfTheWeek.Sunday -> println("$today always comes too late")
}
I don't care if Monday's blue
else -> println("I don't feel like singing")
'when' is exhaustive so 'else' is redundant here
'wrew' am owviihquqe ci 'eqyu' id yocivxejn gaci

I don't feel like singing
It's Friday, I'm in love
'when' expression on enum is recommended to be exhaustive, add 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Saturday', 'Sunday' branches or 'else' branch instead
'jxoq' esbcoxcuiy uj ecer ar qiremkeflon si de ihweiphiwi, esc 'Xetbay', 'Keobmag', 'Xugbupkad', 'Zhizdfas', 'Ledeysas', 'Gutmil' qxehqxez or 'ozqe' dyudmg aklboew

It's Friday, I'm in love

Sealed classes vs. enum classes

As you saw briefly in the previous chapter, a sealed class has a limited number of direct subclasses, all defined in the same file as the sealed class itself. It’s known as sealed as opposed to final, since although some subclassing is permitted (and in fact, required, as you’ll see in a moment), the subclassing is extremely limited in scope.

Creating a sealed class

Imagine you’re working for a company that mostly works in U.S. dollars, but also accepts payments in Euros and some form of cryptocurrency.

sealed class AcceptedCurrency {
  class Dollar: AcceptedCurrency()
  class Euro: AcceptedCurrency()
  class Crypto: AcceptedCurrency()
}
val currency = AcceptedCurrency.Crypto()
println("You've got some $currency!")
You've got some AcceptedCurrency$Crypto@76ed5528!
val name: String
  get() = when (this) {
    is Euro -> "Euro"
    is Dollar -> "Dollars"
    is Crypto -> "NerdCoin"
  }
println("You've got some ${currency.name}!")
You've got some NerdCoin!
sealed class AcceptedCurrency {
  abstract val valueInDollars: Float
  class Dollar: AcceptedCurrency() {
    override val valueInDollars = 1.0f
  }
  class Euro: AcceptedCurrency() {
    override val valueInDollars = 1.25f
  }
  class Crypto: AcceptedCurrency() {
    override val valueInDollars = 2534.92f
  }
  // leave the existing name property alone
}
var amount: Float = 0.0f
fun totalValueInDollars(): Float {
  return amount * valueInDollars
}
currency.amount = .27541f
println("${currency.amount} of ${currency.name} is "
  + "${currency.totalValueInDollars()} in Dollars")
0.27541 of NerdCoin is 698.1423 in Dollars

Enumeration as state machine

A state machine is essentially an exclusive list of possible states for a given system. Using an enum can make it more clear to the caller what state the system is in at any point.

enum class DownloadState {
  Idle,
  Starting,
  InProgress,
  Error,
  Success
}
Downloader().downloadData("foo.com/bar",
  progress = { downloadState ->
    //TODO
  },
  completion = { error, list ->
    // TODO
})
error?.let { println("Got error: ${error.message}") }
list?.let { println("Got list with ${list.size} items") }
when (downloadState) {
  DownloadState.Idle -> println("Download has not yet started.")
  DownloadState.Starting -> println("Starting download...")
  DownloadState.InProgress -> println("Downloading data...")
  DownloadState.Error -> println("An error occurred. Download terminated.")
  DownloadState.Success -> println("Download completed successfully.")
}
"Downloading" from URL: foo.com/bar
Download has not yet started.
Starting download...
Starting download...
Downloading data...
[etc...]
Downloading data...
Got list with 100 items
Download completed successfully.
"Downloading" from URL: foo.com/bar
Download has not yet started.
Starting download...
Starting download...
Downloading data...
Downloading data...
[etc...]
Got error: Your download was eaten by a shark.
An error occurred. Download terminated.

Nullables and enums

Enums can also be dealt with at both the when level and as part of an API with nullability. In the Downloader class, instead of having an Idle option in DownloadState, you could express that nothing was happening by allowing the download state to be optional.

var downloadState: DownloadState? = null
fun downloadData(
  fromUrl: String,
  progress: (state: DownloadState?) -> Unit,
  completion: (error: Error?, data: List<Int>?) -> Unit
) {
  // rest of method unchanged
}
...
private fun postProgress(
  progress: (state: DownloadState?) -> Unit
) {
   // rest of method unchanged
}

when (downloadState) {
  null -> println("No download state yet")
  /// rest of when unchanged
}
"Downloading" from URL: foo.com/bar
No download state yet
Starting download...

Challenges

Key points

  • Enum classes are a powerful tool for handling situations where a piece of data will (or at least should) be one of a defined set of pre-existing values. Enum classes come with a number of tools for free, such as getting a list of all the declared cases, and the ability to access the order and names of the cases.
  • Sealed classes are a powerful tool for handling situations where a piece of data will (or at least should) be one of a defined set of pre existing types.
  • Both enum classes and sealed classes let you take advantage of Kotlin’s powerful when expression to clearly outline how you want to handle various situations.
  • Enum classes are particularly useful for creating, updating, and cleaning information about the current state in a state machine.

Where to go from here?

There are a few more places where you can learn more about enum classes and sealed classes:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now