The Model in MVVM
State, Events and Side-Effects
I have often had the feeling it was not clear to me what the Model was in my apps. While applying the Model-View-Presenter pattern, I implemented View interfaces and Presenter classes, but there was no explicit Model in my code.
A while ago, I took a break from Android development and decided to learn React and CycleJS. It took me some time to get my head around these different ways of structuring front-end applications.
A few months later, back in the Android realm, I started looking into the new ViewModel
and LiveData
classes of the Architecture Components library. I decided to put in practice the concepts I had learnt by using the aforementioned Javascript libraries: I would represent the Model in my Android apps as a State, Events and Side-Effects.
The State
What is the State? The dictionary says it is “the particular condition that (…) something is in at a specific time”. For our purposes, that “something” will be the View.
Events
What is an Event? Again, the dictionary says it is “a thing that happens or takes place”. I would add that, in the context of an application, an Event has the potential to change or modify the State.
Side-Effects
According to the dictionary definition, a Side-Effect is “ a secondary, typically undesirable effect of a drug or medical treatment”… Well, I guess the dictionary definition will not help us this time.
A Side-Effect, in the context of our Model, is something that happens as a consequence of an Event. For example, in the Event of a user pressing a button, a Side-Effect could be the execution of a network request or database query. These Side-Effects could produce a result in the form of an Event (e.g.: Success, Error, etc.).
Show me the code!
In a previous article I provided a very basic example on how to represent streams of State and Events in the context of a ViewModel
using RxJava.
As a consequence of spending time applying the approach and refining it, I wrote a very small Android library called RxModel. It is so small you may as well copy and paste the code into your project. In this article I will provide an example of how to use it to structure the Model in your apps.
Coin Tosser
For this example, we are going to build an application consisting of a screen with a TextView
and a Toss Button
. When the Button
is pressed, it becomes disabled and the TextView
reads “Tossing…” for a second. After that, the Button
is re-enabled and the TextView
reads either “Heads” or “Tails”.
I made a GitHub repository with the implementation of the following example, in case you would like to check it out.
Let’s begin by modeling the Events for this use-case:
sealed class CoinTosserEvent {
object Toss : CoinTosserEvent()
object Heads : CoinTosserEvent()
object Tails : CoinTosserEvent()
}
I have used a sealed class to define 3 types of Events. This way I ensure the application is only going to expect a finite set of Event types.
Next, we will represent the State for the same use-case:
data class CoinTosserState(
val isTossing: Boolean = false,
val isHeads: Boolean = false
)
I used a data class to represent the different properties of the State. The State is immutable: you cannot modify its properties. But how are we going to update the State after an Event occurs? We will we make a copy of the State and change only the properties we need to change (data
classes count with a very handy copy(...)
method that makes this easy).
Which takes us to the following step: defining a Reducer (if you haven’t already, you will need to add RxModel to your project to move forward).
object CoinTosserReducer : Reducer<CoinTosserState, CoinTosserEvent> {
override fun apply(
state: CoinTosserState,
event: CoinTosserEvent
) = when (event) {
CoinTosserEvent.Toss -> state.copy(
isTossing = true
)
CoinTosserEvent.Heads -> state.copy(
isTossing = false,
isHeads = true
)
CoinTosserEvent.Tails -> state.copy(
isTossing = false,
isHeads = false
)
}
}
The Reducer consists of an object with a single function which takes the current State and an incoming Event and returns a new updated State.
So far we expressed what the Model of our app is in a declarative manner. But I purposely introduced a requirement in the specification of the use-case:
When the
Button
is pressed, it becomes disabled and theTextView
reads “Tossing…” for a second.
What the statement above implies is that some work is going to be done in the background for some time and it will eventually produce a result. We are now dealing with asynchrony, which is what RxJava is meant to help us with. Triggering this asynchronous work is what I call a Side-Effect.
Let’s define a concrete Model and declare its Side-Effects:
class CoinTosserModel
: StateEventModel<CoinTosserState, CoinTosserEvent>(
CoinTosserState(),
CoinTosserReducer
) {
private val tossEventsObservable = eventObservable
.ofType(CoinTosserEvent.Toss::class.java)
private val tossSideEffect = Single
.timer(1, TimeUnit.SECONDS)
.map {
when {
Math.random() < .5 -> CoinTosserEvent.Heads
else -> CoinTosserEvent.Tails
}
}
override fun subscribe() = publish(
tossEventsObservable.flatMapSingle { tossSideEffect }
)
}
Notice CoinTosserModel
extends from StateEventModel
, which takes the initial State of the View and a Reducer as its constructor’s parameters.
And now I will explain what is probably the most complex part of this article: binding Events to Side-Effects and Side-Effects to Events.
- The first thing we do is obtaining an Observable of a specific Event type (
Toss
in this case). - Then, we define the Side-Effect to be triggered by that Event type and make sure its possible results are mapped to Events (
Heads
orTails
in this case). - Finally, we bind the Event to the Side-Effect and publish the resulting Event back into the Event stream.
The definition of our Model is complete. We can now use it in our ViewModel
:
class CoinTosserViewModel : ViewModel() {
private val model = CoinTosserModel()
private val disposable = model.subscribe()
val liveState: LiveData<CoinTosserState> =
LiveDataReactiveStreams.fromPublisher(
model.stateObservable.toFlowable(
BackpressureStrategy.LATEST
)
)
override fun onCleared() {
super.onCleared()
disposable.dispose()
}
fun onToss() {
model.publish(CoinTosserEvent.Toss)
}
}
We expose the State to the View as LiveData
so we do not have to deal with the intricacies of the Activity (or Fragment) life-cycle. We subscribe to the Model and dispose of the subscription when the ViewModel is cleared to avoid leaks that could be produced by ongoing asynchronous work related to the Side-Effects we trigger.
The onToss()
method will be called by the View as you can see below:
class CoinTosserActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coin_tosser)
val viewModel = ViewModelProviders.of(this)
.get(CoinTosserViewModel::class.java) viewModel.liveState.observe(this, Observer {
it?.apply {
statusTextView.text = when{
isTossing -> getString(R.string.tossing)
isHeads -> getString(R.string.heads)
else -> getString(R.string.tails)
}
tossButton.isEnabled = !isTossing
}
})
tossButton.setOnClickListener { viewModel.onToss() }
}
}
The View observes the State and updates accordingly. When the Toss button is clicked, it lets the ViewModel
know.
That’s it! The cycle is complete. I hope this approach opens up more possibilities to what you can achieve in your apps. Happy coding!