Kotlin sealed class with generic covariance
By: Aleksei Jegorov
Tags: Generics, Sealed Classes, Covariance, Kotlin, When

Covariance as a contract with strict rules make class type predictable and safe.

Covariance approach let us to narrow the type of certain method, by example: Int type is a part of super type Any.

Kotlin provides ‘In’, ‘out’ reserved keywords — to enhance our tools of using covariance/contravariance.

For ‘in’ generic, we could assign a class of super-type to class of sub-type. But for ‘out’ generic, we could assign a class of sub-type to class of super-type.

If our class used generic type as the output of the function, we use ‘ out ’ in covariance logic. We pass type as a parameter to the Interface to be strict and concise in terms of functionality we use. ‘ Out ’ indicates that T is intended to be produced by the methods of this class.

In fact,

1. Use in when super type could be assigned sub type.

2. Use out when sub type could be assigned to super type.

With courtesy of Kt.Academy by Marcin Moskala

By example, in real world we need to show a set of possibilities: class with a methods about Success, Failure or Proceeding state.

Sure, first thing we can use enum . But enum have their limitations: each sub type can be only a constant and it has no state.

Interface

Let’s start to investigate from the beginning. In most simple case we can use regular approach —interface as a contract of available methods. We override predefined methods in custom class:

Also we can use classic try {} catch {} finally {}. It often makes an unreadable mess like this:

But such code is hard to read. We overfilled with a bundle of code and sometimes not so obvious to get certain exception of particular method.

Kotlin provides its own approach.

It is a sealed class.

We have different types of nested classes and remain the state. Sealed class allows us to represent hierarchies. And it prevents inheritance, this class is sealed, so use nested child classes.

Sealed class over abstract class:

Sealed class is abstract by default, so we can define nested child classes of the parent sealed class.

Sure, we can use simple plain sealed class, but it is so boring. More interesting is to play with Generics and pass type as parameter.

Generics hierarchy allows us to be more flexible, because most of these result class hierarchies look pretty similar: the same names and properties.

Covariance provides keyword out when sub type could be assigned to super type. It motivates us to work with that type and be type-safe.

My full example looks like this:

Additional bonus for when keyword.

When expression must be exhaustive, add necessary ‘Loading’ branch or else branch instead. It can be applied to enforce that a when statement is exhaustive.

val <T> T.exhaustive: T

get() = this

This extension property helps us not to get a error from the editor and compiler: “‘when’ expression must be exhaustive, add necessary ‘is Error’ branch or ‘else’ branch instead”

when(val result = getResult()) {

is Result.Success -> println(“succeeded”)

is Result.Error -> println(result.cause?.message)

}.exhaustive

Deep into the sea of code by stairs…

Conclusion

Use sealed class to avoid type errors and follow the predefined methods.