Joao Alves

Engineering Manager at Skyscanner

Kotlin Sealed Classes — enums with swag

Posted at — Feb 16, 2018

image

Hi there everyone, welcome to article number 6 on the series. Today we’ll be looking at Kotlin Sealed classes and how they can take enums to a completely different level and add them some swag. Let’s start with a basic enum in Java and see how it looks in Kotlin:

public enum BasicScreenStateJava {
    ERROR,
    LOADING,
    DATA
}
enum class BasicScreenState {
    ERROR,
    LOADING,
    DATA
}

Pretty simple and pretty similar right? Enums are already pretty concise in Java so Kotlin doesn’t save us any boilerplate with these. Regarding functionality, they’re also very simple as there’s not much we can do with them.

Here’s an example of how we would use our enum in our view to set the new state passed from a presenter for example:

fun setBasicScreenState(basicScreenState: BasicScreenState) {
    when(basicScreenState) {
        BasicScreenState.ERROR -> { /* set error state in the view */ }
        BasicScreenState.LOADING -> { /* set loading state in the view */ }
        BasicScreenState.DATA -> { /* hide loading or error states in the view */ }
    }
}

fun displayData(someData: SomeData) {
    // actually display the data in the view
    //sometextView.text = someData.name
}

As you can see we actually need 2 different functions to handle our success case. In that case, our presenter would need to explicitly call both setBasicScreenState() and displayData() functions to set a proper state in the view. This happens because enums are super simple and not capable of holding data in a way that is helpful to us.

enum class ScreenStateField(val someData: SomeData) {
    ERROR(SomeData("1", "some data 1")),
    LOADING(SomeData("2", "some data 2")),
    DATA(SomeData("3", "some data 3"))
}

Enums can actually have fields but you need to call the same constructor on all of them which pretty much defeats the purpose of it unless you have a very very specific use case. In our case, this doesn’t help us as at all. If we want our SomeData result passed on the DATA state we would need to also pass it on the other states which is not only unnecessary as it’s bad because the only thing we can pass is dummy data assuming we don’t actually have any while Loading or after an Error occurs.

So, in essence, if we want to use enums for something simple that doesn’t need to hold any data or different types of data per value they’re fine but if we need something more specific like our example, we need something else.

And this is where Kotlin sealed classes can help us, let’s see how. Before getting into code let’s take some sentences from the official docs that can help define a Kotlin Sealed Class:

Sealed classes are used for representing restricted class hierarchies.> They are, in a sense, an extension of enum classes.\

A sealed class can have subclasses, but all of them must be declared in the same file.> A subclass of a sealed class can have multiple instances which can contain state.\

You can declare the subclasses inside the sealed class or outside but they always have to be declared in the same file.\

A sealed class is abstract by itself, it cannot be instantiated directly and can have abstract members.\

Sealed classes are not allowed to have non-private constructors (their constructors are private by default).

Some big differences from enums in there. Sealed classes can be inherited from, which is not the case with enums that are final. Their subclasses can have multiple instances while enum constants are basically singletons. And subclasses can also contain different states.

Now that we know more about Kotlin Sealed classes let’s see how they add some swag to enums and can help us with our problem:

sealed class ScreenState {
    class Error : ScreenState()
    class Loading : ScreenState()
    data class Data(val someData: SomeData) : ScreenState()
}

Ok, so now it looks like we have what we need. Simple Error and Loading states with no data, and a Data state actually holding the data to display in the view.

Notice how the Data state is actually a data class? The reason is, if we want to compare your ScreenState for example in a unit test or for any other reason we can take advantage of the equals implementation a data class gives us for free. I didn’t make Error and Loading data classes but they can also be for consistency if you prefer. If they hold some kind of data then I actually suggest you should use data classes all the time.

But in the other hand, as Roman Bielokon suggested in a comment below, for Error and Loading because they don’t hold any data it’s actually better to have them as singletons, which I think is a good idea. This is how their declaration would look like instead:

object Error : ScreenState()  
object Loading : ScreenState()

And this is how it looks when we’re using our sealed class hierarchy it in the view:

fun setScreenState(screenState: ScreenState) {
    when(screenState) {
        is ScreenState.Error -> { /* set error state in the view */ }
        is ScreenState.Loading -> { /* set loading state in the view */ }
        is ScreenState.Data -> {
            /* hide loading or error states in the view and display data*/
            //sometextView.text = screenState.someData.name
        }
    }
}

Cool, no more separate methods to set state and display the data in case of success. Our Data state can now handle both. Cleaner and easier to read, right? But this is not all, there are other benefits of using Sealed classes over enums.

Take a look at the when statement above. If we use it as an expression instead and store the state in a variable, we take full advantage of the when expression and get compilation errors if we don’t handle all possible branches or add an else branch.

Also because of smart casts, we can set the text view directly with screenState.someData.name as the compiler already knows that it’s handling a ScreenState.Data instance.

If we use enums in Java we can use a switch statement and get some of this, but we don’t get compilation errors if we don’t implement one or more branches. And if we want to use some form of class hierarchy in Java to replicate the same thing we have with Sealed classes we can’t even use a switch statement. So, next time you need an enum with some extra swag and not sure how to do it, or you need some form of restricted class hierarchy give it a try with Kotlin Sealed classes.

I can not finish this one without leaving a special thank you to my colleague Mikołaj for the amazing presentation he did for us at Babylon on Kotlin Sealed classes. Presentation that pretty much inspired this article and from where I stole a lot of stuff, with his consent to be noted 😄

And that’s it for today :) If you enjoyed it don’t forget to 👏 and please share your ideas and comments as usual. The code is available under the sealedclasses package on the series project. See you at number 6 👋

Parcelable in Kotlin? Here comes Parcelize ⇦ PREVIOUS

NEXT ⇨ Kotlin Static Analysis — why and how?

comments powered by Disqus