MVI Pattern in Android without RxJava

Using Kotlin coroutines and Android Jetpack to model a Unidirectional State Flow pattern.

I have been working with RxJava for the past one and a half year, and at first glance, I still don’t totally understand how reactive code flows. Some of it must be my shortcoming but I believe, part of the reason is that it’s inherently hard to read. With all the transformers and thread switching with subscribeOn and observeOn, there is a cognitive overload when you read it.

But I really like the Unidirectional State Flow pattern aka MVI (Model/View/Intent) architecture for all the reasons that articles such as this explain in great detail. So, I have been working with a form of MVI architecture that doesn’t involve any RxJava or other reactive streams API (other than LiveData). You can call it MVI-Lite.

There are quite a few flavours of MVI architecture for Android but I especially like the one that Kaushik Gopal spoke about on his popular Android podcast, Fragmented. I forked his sample project and refactored Rx out of it, replacing it with LiveData and Kotlin coroutines. For a visual explanation of the Unidirectional pattern, check out Kaushik’s slides.

The sample app created to demonstrate this pattern is a simple single screen app that allows the user to search for a movie and displays the result with the movie’s poster, title and rating. Tapping on the poster saves the movie to history which is displayed at the bottom of the screen as a horizontal list. Tapping on a movie from the history list restores it back to the top search result.

All the refactored code detailed below can be found here.

Events → Result → ViewState

To demonstrate the flow of data, let’s say, it starts with the user-facing View or Screen.

When user interacts with the View , instances of Events are generated and passed on to the ViewModel . These Events are modelled as a sealed class.

The Events are created upon any user interaction or system level changes on the View and are propagated to the ViewModel by calling an onEvent() function.

The ViewModel acts upon these events accordingly by making API calls or saving/retrieving data in the database via the Repository layer.

Any potentially long running tasks are executed in a background thread by the Repository in suspending functions. These functions are called from a coroutine builder in the ViewModel and there is no need to switch threads in these coroutines. They execute on the main thread and suspend execution when context switches to a background thread in the Repository . For more on coroutines, check out the excellent official guide. A suspending function in the Repository would look like -

Upon completion of these suspending functions, the ViewModel receives success or failure response from the Repository layer which is transformed into an LCE<Result> (Loading/Content/Error), thus triaging the response according to its status. Result is, again, a sealed class. (More on LCE here)

These Result objects contain information that needs to be conveyed to the View. This is where my favourite part of this pattern comes in. Whatever the View shows on screen is represented by a ViewState , which is a simple data class. The ViewState is immutable and at any given moment the View is being represented by a single ViewState . When info from a new Result is to be propagated to the View, a new ViewState is created by copying the current ViewState and changing the necessary bits according to the Result . For example, if an API call has been successful then isLoading is set to false and the necessary data fields are populated.

The current ViewState is held by the ViewModel as a property and is exposed to the View through a LivaData<ViewState> . The View observes this LiveData and renders itself.

The part I like most about Kaushik’s solution is the addition of ViewEffects . These are one-off triggers that the View needs to act on, but they do not need to be persisted. For example, a toast is a ViewEffect that needs to happen only once, if persisted, it will show again when the orientation is changed or the View is restarted. ViewEffects are also exposed to the View through LiveData but unlike ViewState , the current ViewEffect is not stored as a property in the ViewModel .

And that’s it. The cycle is complete, starting from the view and ending at the view. At any given moment, we can easily track and debug what state the cycle is in by printing Events , Results and ViewStates .

The project also includes some basic Unit Tests, which could be expanded on in a Part 2. Thanks for reading.