Kotlin Sealed Classes

In this Android tutorial, see how to use Kotlin sealed classes to create limited hierarchies that act like enums but allow you to create multiple instances. By Joe Howard.

Leave a rating/review
Download materials
Save for later
Share

Unlike most other object-oriented languages, Kotlin classes are final by default. This goes along with a principle from Effective Java, 3rd edition, “Design and document for inheritance or else prohibit it.” In Kotlin, you must explicitly mark a class open in order to allow inheritance from it.

Falling in a sense in-between these two extremes of open and final, there is a form of inheritance available in Kotlin named Sealed Classes. A sealed class has a limited number of direct subclasses, all defined in the same file as the sealed class itself. Sealed classes let you take advantage of some of the flexibility of inheritance, without ending up with a massive inheritance tree. Sealed classes act very much like a more powerful form of enum classes, with the sealed class subtypes being the cases of the enum. Unlike enum cases, however, sealed class subtypes can have multiple instances.

Sealed classes themselves are abstract, so you can’t instantiate an instance of the sealed class directly, only the subclasses. Sealed classes can also have abstract members, which must be implemented by all subclasses of the sealed class. You can create indirect subclasses outside the file where the sealed class is declared, by inheriting from one of the subclasses, but this usually doesn’t end up working very well. In this tutorial you’ll see these properties of sealed classes in action in a currency converter app.

The material in this tutorial was adapted from a section of our book Kotlin Apprentice written by Ellen Shapiro.

Getting Started

Use the Download Materials button at the top or bottom of the tutorial to download and unzip the sample code for this tutorial. Open the starter project in Android Studio 3.2.1 or later.

Once the Gradle build completes, build and run the starter app.

The Starter App

There are two fields for a low amount of money and a high amount of money, and a dropdown to choose a currency for the amounts, either US Dollars, Euro, or a Cryptocurrency. There’s also a convert button to show the equivalent amounts in US Dollars.

Creating a Sealed Class

To begin working with sealed classes, right-click on the app main package and choose New ▸ Kotlin File/Class name it AcceptedCurrency.

Now add a new class to the file and mark it with the sealed keyword to make it a sealed class.

sealed class AcceptedCurrency

Then add three subclasses directly to same file:

class Dollar : AcceptedCurrency()

class Euro : AcceptedCurrency()

class Crypto : AcceptedCurrency()

Unlike early versions of Kotlin, since version 1.1 the subclasses only need to be in the same file as the parent, and no longer need to be within its body.

Open up ConverterActivity and update the list of currencies to be a list of AcceptedCurrency subtype instances and not strings.

private val currencies = listOf(Dollar(), Euro(), Crypto())

You also need to change the type of the ArrayAdapter for the spinner dropdown.

val adapter = ArrayAdapter<AcceptedCurrency>(...)

Build and run and the app.

Sealed Class strings

Looking at the dropdown, you no longer see strings. If we were using enum values in the dropdown, enum classes have a name property that would be used in its string version. For sealed classes, you lose things like a nice name and ordering.

Sealed Class Properties

Fortunately, sealed classes can have non-abstract properties with custom getters, and can also take advantage of when expressions.

Inside the AcceptedCurrency sealed class body, add a name property with a custom getter:

sealed class AcceptedCurrency {
  val name: String
    get() = when (this) {
      is Euro -> "Euro"
      is Dollar -> "Dollars"
      is Crypto -> "NerdCoin"
    }
}

Like for an enum, the when expression needs to cover all the possible sealed class subtypes, or else you’ll get a compiler error.

Switch the adapter in ConverterActivity back to a String type and use map to map each currency to its name for the dropdown:

val adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item,
    currencies.map { it.name })

Build and run the app, and you see the sealed class names again in the dropdown.

Showing the name

Abstract Properties

Imagine that the company for this app is U.S.-based, so they want to know how much each of these currencies is worth in US Dollars.

Update the AcceptedCurrency sealed class to add an abstract val property named valueInDollars, and then override it in the all three declared subclasses:

abstract val valueInDollars: Double
class Dollar : AcceptedCurrency() {
  override val valueInDollars = 1.0
}

class Euro : AcceptedCurrency() {
  override val valueInDollars = 1.25
}

class Crypto : AcceptedCurrency() {
  override val valueInDollars = 2534.92
}

The valueInDollars property captures the exchange rate between the currencies.

It would probably also help to know how much of a currency is being passed around with a single instance. You can add non-abstract properties to a sealed class, as long as you provide them with an initial value.

Right below your abstract declaration of valueInDollars, add a new variable amount and initialize it to zero:

var amount: Double = 0.0

Now that you have a place to store the value of a particular currency, you can calculate the total value of the accepted currency. You’ll do that by adding a non-abstract function to your sealed class.

Since every AcceptedCurrency subclass must provide a valueInDollars property, and all subclasses have access to the amount property you just added, you can use those at the AcceptedCurrency level to provide the same functionality across all classes.

Below the name property, add a new function to calculate the total value in dollars of a given currency:

fun totalValueInDollars(): Double {
  return amount * valueInDollars
}

Now go back to ConverterActivity and add a function that will pick the current currency based on the selection in the spinner dropdown:

private fun currencyFromSelection() =
    when (currencies[currency.selectedItemPosition]) {
      is Dollar -> Dollar()
      is Euro -> Euro()
      is Crypto -> Crypto()
    }

Now that you can determine the desired currency based on the selection, you want to be able to convert the amounts entered by the user into that currency. Since there are two amounts, you’ll need to create multiple instances of a currency type.

Thankfully, sealed classes support the creation of multiple instances of the subtypes.