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
Recap on observing view state
You can observe any changes that are generated in your
ViewModel by using a Kotlin data class and
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.
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 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
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.
ViewModel, we can allow the
Activity to observe any new commands issued by the
Issuing a command
ViewModel, we can issue a command by pushing it out through
Note, when we instantiate each type of command, we can provide it with its own preferred data.
Processing a command
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
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.