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.

Version

  • Kotlin 1.3, Android 4.4, Android Studio 3

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.

Subtype Instances

Replace the code in convert.setOnClickListener, and create two AcceptedCurrency instances using the new function

convert.setOnClickListener {
  val low = currencyFromSelection()
  val high = currencyFromSelection()
}

These two values are instances of the same AcceptedCurrency subtype, the one that’s selected in the spinner. You see here that unlike enum cases, you can create multiple instances of a sealed class subtype.

Finish off the convert button click listener by converting the amounts in the fields to amounts on the AcceptedCurrency instances, and then show those amounts in dollars in the text views using the totalValueInDollars() function of the parent sealed class:

low.amount = lowAmount.text.toString().toDouble()
high.amount = highAmount.text.toString().toDouble()

lowAmountInDollars.text = String.format("$%.2f", low.totalValueInDollars())
highAmountInDollars.text = String.format("$%.2f", high.totalValueInDollars())

Build and run the app.

Enter low and high values and choose a non-dollar currency. Then click convert and you’ll see the equivalent dollar amounts.

The Final App

Congratulations! You’re now the possessor of some supreme knowledge on Kotlin sealed classes!

Congratulations!

Where to Go From Here?

You can use the Download Materials button at the top or bottom of the tutorial to access the final version of the currency converter app.

You’ve seen how sealed classes let you build restricted hierarchies for a finite set of states, in this case a set of currencies.

Another common use for sealed classes is tracking the loading state of a screen, e.g. loading, empty, or done.

Sealed classes come in very handy in architecture patterns like Model-View-Intent, in which a stream of user intents (be careful—not the Android Intent class) get translated to different types of representations as they are processed by the app. Sealed classes are a match for handling those different representations.

Check out the official documentation to learn more about sealed classes.

If you have any questions or comments, please join the discussion below!

Add a rating for this content

Contributors

Comments