Android has been out for over a dozen years now, and how we write applications for Android has changed over the years. From the host of architectural solutions that have been proposed over the years, MVP (Model View Presenter), MVVM (Model View, ViewModel) and more recently MVI (Model View Intent) are the most notable ones. With the release of the Android Architecture components at Google IO 2017 the spotlight was on MVVM and that’s been the preferred architecture until recently.
With the recently release of JetPack Compose Beta and the required mental shift on how we write apps for compose, especially state management, a new architecture solution seems to be making the rounds these days, MVI. Before we delve into how to implement MVI on Android with coroutines, let’s quickly overview how MVVM and MVI work and how they differ from one another. Both MVVM and MVI share the Model and View parts, so here we will only be focusing on the ViewModel/Intent part.
On MVVM the ViewModel exposes a set of variables, usually backed by LiveData, that represent a concrete piece of the view state. The view then observes these variables and changes to these variables drive the UI. It is not uncommon to have a fairly large number of observable variables for complex screens, which makes managing them fairly challenging, especially when there are dependencies between variables. Furthermore, to hide implementation details and expose only immutable variables, we usually duplicate state variables by having a private mutable version and a public immutable one. Further compounding the complexity is the fact that in MVVM there isn’t a centralized place to handle the state, making this harder to maintain and test, as variables could be changed in different methods of the ViewModel.
An example of a ViewModel for a city search screen would look something like this:
While this only contains 3 state variables, it is easy to see how this number can grow and become rather unwieldy.
The idea behind MVI is that the the ViewModel exposes the state of the screen as a whole, as opposed to as a set of individual variables. There is also a single place where the state is updated, making it easy to read and maintain the code. The code is also more testable as we can write test functions that ensure the state is always valid (say, the error message is only provided if we are in an error state, not in a loaded state). MVI shares some commonality with the Redux framework, the reducer is the method responsible for creating a new state based on the current state and the user actions, called
Following from the example above, the city search ViewModel could be rewritten as shown below
This already looks a lot more manageable and concise. We can see it will be easy to validate the state as we have a single variable to represent the screen, so we could write unit tests that ensure the state is always valid.
So, back to the original point of this article, how would we go about implementing MVI using Kotlin coroutines? For MVI we need to expose a method that the View will call for user actions, the
Intents; we will call this
onIntent. The View needs to observe the
State from the ViewModel, and the ViewModel needs to generate new state from each
Intent it receives from the View. The state will be exposed from the ViewModel using a
Using Kotlin flows, we can create a base ViewModel class as shown below
The state is exposed as a
StateFlow that the view can observe. When the user triggers and action this calls into the
onIntent method, where we pass as argument the
MviIntent is just a marker class that will be implemented as
sealed classes representing each of the user
OnIntent method is a
suspend function, and this may trigger any number of
ReduceActions, which will trigger the generation of a new state. For instance, as a result of a user action we may emit the
Loading action while we are executing the
Intent, and once that intent completes we may emit a
Error action as a result. Each of the emissions during
onIntent will update the state. The flow below shows how the states are generated as a result of
With this base class, we can rewrite our cities search viewmodel as shown below
The corresponding fragment is shown below
We can see that:
- whenever the user updates the city search query, we trigger a new
Intentthat carries the query string
- when this
Intentis received in the ViewModel, if the length is 3 or greater, we trigger a Load reducer action, which is handled in the
handlemethod and will update the state to show a loading spinner. After emitting this initial action we execute the search query, and when that completes we emit either a a success or failure reducer action which will trigger a new state where we stop the loading spinner and either show the cities result or an error message.
This approach works very well with Jetpack Compose as we have a
State exposed from the ViewModel that composables can collect using the
collectAsState extension function.
A full MVI implementation is available on this repo, it includes both an implementation for the view system and one for Jetpack Compose.