Issuing Commands from a ViewModel using Kotlin Sealed Classes

Using Android Architecture Components, you can have your Activity easily observe changes in your ViewModel. But sometimes you want your ViewModel to issue a command and have your Activity obey. This article will describe how you can use Kotlin sealed classes to represent these commands.

Let’s say you are building a web browser, and you have a BrowserActivity and a BrowserViewModel.

Recap on observing view state

You can observe any changes that are generated in your ViewModel by using a Kotlin data class and LiveData.

The Activity would observe view state changes 👆 and you can represent the view state using a kotlin data class 👇.

You can read more about this technique in Representing View State with Kotlin Data Classes.

Commands

But what about commands? By commands, I mean that as a result of some logic in your view model, your ViewModel needs to tell the Activity to do something.

In our browser, such commands might include instructing your WebView to refresh, navigating to a certain URL or sending a system Intent to send an email. We want to keep logic out of the Activity and have the ViewModel be responsible for deciding what to do. Ultimately though, we need to have the Activity obey the command as that is what has access to the rest of the Android system.

Kotlin has a feature which is perfect for representing these commands; sealed classes.

Sealed Classes

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.

While we could try to represent our commands as enums, we’ll run into a problem; each command might have different data that it holds. Our “navigate to web page” command might hold a Uri, our “send email” command might need a String, our “refresh web view” command would not need any additional data.

The beauty of a Kotlin sealed class is that it lets us define commands such that each hold data that makes sense for them, and yet all the commands are still related to each other.

Defining a sealed class

Here, we declare a Command class using the sealed keyword. We can then list the various command types that we need. For each new command type we define, we need to extend the Command class using : Command() syntax.

By doing this, we’ve created a class hierarchy. We can now have code which expects to receive a Command and will receive compile-time safety in ensuring a command must be of the types defined.

Sending command only once

In the ViewModel, we can expose the commands using LiveData. However, commands are slightly different from normal view state, in that once a command has been issued and received, we don’t want it to ever happen again. If the user rotates their phone for instance, we don’t want the new Activity to receive the command a second time.

As such, I make use of SingleLiveEvent which is like LiveData, except it is a one-time occurrence.

In our ViewModel, we can allow the Activity to observe any new commands issued by the ViewModel.

Issuing a command

In the ViewModel, we can issue a command by pushing it out through LiveData.

Note, when we instantiate each type of command, we can provide it with its own preferred data.

Processing a command

In the Activity, we can react to any commands being issued by observing for new commands.

We make use of Kotlin’s when operator, which allows us to receive a Command object and then elegantly decide which command subtype we are dealing with.

And the real beauty here is that Kotlin automatically knows which data type you have inside the when blocks; it has already cast the type for you automatically. Therefore, when you are dealing with the SendEmail command, you can use it.emailAddress . When you are dealing with the DialNumber command, you can use it.telephoneNumber.

Summary

By combining LiveData with Kotlin sealed classes, you can represent commands through which the ViewModel can instruct the Activity to perform a certain action. Each command can define their own data — even using different data types from each other. And using the when operator we can elegantly decide how to process each command.