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.