Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.
https://kotlinlang.org/docs/reference/sealed-classes.html
If you are like me, you probably did not understand that on a first read, especially without
If you come from a Java background, then you should be familiar with Enums. Sealed classes are essentially Enums v2. Before we begin let’s take a look at the sealed class rules.
Sealed class rules
- Sealed classes are abstract and can have abstract members.
- Sealed classes cannot be instantiated directly.
- Sealed classes cannot have public constructors (The constructors are private by default).
- Sealed classes can have subclasses, but they must either be in the same file or nested inside of the sealed class declaration.
- Sealed classes subclass can have subclasses outside of the sealed class file.
Declaring a sealed class
sealed class Fruit()
The important part of the code above is the `sealed` modifier. As mentioned in the sealed class rules, a sealed class can have subclasses but they must all be in the same file or nested inside of the sealed class declaration. However, subclasses of the sealed class subclasses do not have to be in the same file.
// Nested sealed class Fruit() { class Apple() : Fruit() class Orange() : Fruit() }
sealed class Fruit // Not nested class Apple() : Fruit() class Orange() : Fruit()
// Fruits.kt sealed class Fruit() { class Apple() : Fruit() class Orange() : Fruit() open class UnknownFruit(): Fruit() } // SomeOtherFile.kt class Grape : Fruit() // Not Acceptable class Tomato : UnknownFruit() // Acceptable
The key benefit of using sealed classes comes into play when you use them in a
https://kotlinlang.org/docs/reference/sealed-classes.htmlwhen
expression. If it’s possible to verify that the statement covers all cases, you don’t need to add anelse
clause to the statement. However, this works only if you usewhen
as an expression (using the result) and not as a statement.
For example:
// Invalid override fun getItemViewType(position: Int): Int { return when (fruits[position]) { is Fruit.Apple -> R.layout.item_apple } }
The piece of code above fails because we MUST implement ALL types. The error thrown says:
When expression must be exhaustive, add necessary 'is Orange' branch or else branch instead
This just means that we must include all types of Fruit in the when expression. Or use a catch all else block.
// Valid override fun getItemViewType(position: Int): Int { return when (fruits[position]) { is Fruit.Apple -> R.layout.item_apple is Fruit.Orange -> R.layout.item_orange } }
Bonus: how can we apply sealed classes in a real-world scenario in Android?
There are many ways that sealed classes can be used, but one of my favorites is in a RecyclerView adapter.
Let’s say we want to display a list of fruits in a RecyclerView, where each fruit has its own ViewHolder.
Apple has anAppleViewHolder
and Orange has an OrangeViewHolder
. but we also want to make sure that every new Fruit added has its own ViewHolder explicitly specified, or we throw an exception. We can easily do this with selaed classes.
Show me the code:
sealed class Fruit { class Apple : Fruit() class Orange : Fruit() } class FruitAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val fruits = arrayListOf<Fruit>() override fun getItemCount() = fruits.size override fun getItemViewType(position: Int): Int { return when (fruits[position]) { is Fruit.Apple -> R.layout.item_apple is Fruit.Orange -> R.layout.item_orange } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val layoutInflater = LayoutInflater.from(parent.context) return when (viewType) { R.layout.item_apple -> { val itemView = layoutInflater.inflate(R.layout.item_apple, parent, false) AppleViewHolder(itemView) } R.layout.item_orange -> { val itemView = layoutInflater.inflate(R.layout.item_orange, parent, false) OrangeViewHolder(itemView) } else -> throw UnknownViewTypeException("Unknown view type $viewType") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val fruit = fruits[position] when (fruit) { is Fruit.Apple -> (holder as AppleViewHolder).bind(fruit) is Fruit.Orange -> (holder as OrangeViewHolder).bind(fruit) } } // ... other methods }