Implementing MVI on Android with Coroutines

Background

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.

MVVM

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.

MVI

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 Intents.

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 StateFlow.

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 . MviIntent is just a marker class that will be implemented as sealed classes representing each of the user Intents. The 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 Loaded or 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 Intents:

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 Intent that carries the query string
  • when this Intent is received in the ViewModel, if the length is 3 or greater, we trigger a Load reducer action, which is handled in the handle method 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.

Senior Android Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store