MVI on Android with LiveData Coroutines

Ryan Rawlinson
ProAndroidDev
Published in
5 min readMay 14, 2019

--

Let the user decide what is “intended” to show up on your view. — Featured on zync.ca

A common problem in Android development is state management. There are several design patterns that aim to solve this problem. MVVM (Model-View-ViewModel) uses observables to update the UI in a reactive manner, but the pattern itself can lead to differences in your view state depending on the amount of asynchronous process running at any given time. How do we fix this? Enter MVI (Model-View-Intent). The goal of this article is not to give an in depth explanation of MVI and the usefulness of the pattern, but rather give an example of an implementation that does not require RxJava (which is very popular for MVI).

MVI: A Brief Introduction

MVI (Model-View-Intent) is a pattern that uses immutability and a unidirectional data flow to present an apps UI to users.

The reactiveness of MVI comes from a user committing intents and building up ViewState. One thing to note about intents, these are not what you would think of as Android Intents, but rather intentions to do something (click a button, swipe to delete, etc.) executed by the user. ViewState (the state of your view) is represented by a Kotlin data class(or Java POJO if you are still using Java) and is a literal translation of the states (i.e. loading, saving, empty, or full of data) your screen can be in. When a user commits an intent, that intent is then passed to the ViewModel and is then processed into an Action which will go do whatever it needs to (i.e. save to the database, make a network request, etc.) and returns a result. The result is then reduced into a new ViewState and returned to the view to update the screen for the user.

This unidirectional flow ensures that the user knows what state their view will be in a at all times. A popular way to do this is using RxJava. But, the downside is that just adding the necessary RxJava libraries adds a decent amount of methods to the dex limit and everything that it provides may be unnecessary. Enter Kotlin Coroutines.

Using Coroutines with LiveData

At Google I/O 2019, the AAC (Android Architecture Components) team introduced an update to LiveData using Coroutines. Check out the talk here. The team added a bunch of features regarding coroutines and scoping them with ViewModel. But, I want to focus on a feature called the LiveData builder. The LiveData builder is a DSL built around LiveData that will create a LiveData object and emit updates to the value within the given code block on a LiveDataScope. This block is tied to the LiveData lifecycle and will be created and cleaned up when the Lifecycle Owner is not active/destroyed. The block containing your code is suspended until the LiveData object becomes active and then the code is executed synchronously until there are no more steps to execute. After the execution is complete, the coroutine is disposed with no management involved from the developer. Let’s see this in action with MVI.

MVI Using Coroutines with LiveData

This test app is going to fetch a list of movies (I know so original haha) from TMDB and display them back to the user. I am not going to focus on the app implementation as a whole (I will include a link to my GitHub that has the project), but rather I will focus on the MVI pattern and the LiveData builder.

Let’s begin with our Model and ViewState class.

You can see we have a data class that represents our data model for the Movie object. We also have a data class that represents the different states that our screen can be in (loading, data, and error). Next, we use our ViewModel to create an initial instance of our ViewState and a LiveData instance of our ViewState that can be observed by the fragment.

The ViewModel will leverage a LiveData transformation to perform an Action via a dispatcher for the given intent. The intent in this case is implicit and is executed when the LiveData object is attached so that it can be populated with some initial data. When the result of the dispatch is given back to the LiveData object a when expression is used to exhaustively run through all of the results and update with the appropriate ViewState. If the result is of type MovieResult.Loading then a new ViewState is returned with the isLoading property set equal to true. If the result is of type MovieResult.Success then a new ViewState is returned with the movies property set equal to the data returned in the result. And if the result is of type MovieResult.Failure then a new ViewState is returned with the error property set equal to the error returned in the result.

I leverage a Kotlin sealed class to represent the different type of results for movies. This approach will require a when expression to be exhaustive, otherwise an error will be thrown at compile time.

I created a dispatcher that will receive an action from the ViewModel and will return a result. The action is also based on a sealed class and it will provide different actions that can be committed by the user. When the dispatcher receives an action of type MoviesAction.GetMoviesAction it will begin a process of emissions. This is where the power of LiveData with Coroutines comes in! When the emit() function is executed, it returns a MoviesResult LiveData object that is observed by the transformation in our ViewModel. Emit will set the value of the LiveData object in the background on a scoped coroutine. Once the value is emitted, our ViewModel will take over and return a new ViewState based on the result. First, we emit a result of type MoviesResult.Loading. Then, inside of our second emit, we execute the getPopularMovies(). This function will reach out to our remote source and return a list of movies. Once this operation is complete, we will return a result of type MoviesResult.Success with our new list of movies as the payload for the result. Our ViewModel will then take over and return a new ViewState based on the result.

Back in our fragment we are observing the state in our ViewModel. Each time this state is updated by the dispatcher our render() function is called. This function receives a new MoviesViewState and updates the view accordingly. That’s it!

Wrap-Up

LiveData with Coroutines is a powerful feature that cleans up a lot of the scope management developers were having to implement in their apps. This new feature is currently in alpha and is expected to be stable later this year. Give it a shot and let me know what you think and how you are using it in your apps. Below is a link to the repository for a full implementation.

Cheers!

--

--