Android MVI architecture with LiveData (no RxJava)

Minh Thành Bùi
4 min readJun 11, 2019

--

This is summary of my work to implement an Android prototype with MVI architecture, only with LiveData and no RxJava…

Background

I did react-native before. With redux, redux-thunk, react-native-redux MVI is so simple. I can dispatch any actions (sync or async) and easily access a global state from any components. So I expect to achieve the same approach with Android MVI:

  • Dispatch action from View.
  • Handle both sync action and async action (e.g action with IO operations).
  • Send action results to a reducer which will then produce the new state.
  • All Fragments can access the state.

And I would like to use LiveData because:

  • Officially supported by Google
  • More simple than RxJava
  • There are no dominant MVI frameworks on Android (that make me feel comfortable to stick with)

Prerequisite

This article assumes you are familiar with:

  • MVI architecture flow
  • RxJava concepts: Observable, map and switchMap
  • Android concepts: DataBinding, ViewModel, LiveData
  • Kotlin coroutine
  • Kotlin basic (extension fun, generic…)

(Please refer to the documents at the end of the article)

Okayyyy let’s get into the implementation then :D

Flow of Dispatch - Handle - Reduce

Data flow through them like this:

Example: Data flow through Dispatcher, Handler and Reducer

The implementation:

Let’s see how things go:

  • dispatch assign nextAction.value to action (line 14), which will trigger handle function, because Transformations.switchMap listens for changes of nextAction and convert it to another LiveData — returned by handle (line 6–8).
  • handle is a coroutine, which is a nice place for async API call, we will see it later.
  • The result of handle is watched by Transformations.map. When ready, result is passed to reduce function to produce a new state (line 6–10).
  • In the code below, fragment observes the viewState and re-render the View when new state is ready (line 15–17). You can also see how action is dispatched from fragment (line 38)

The important point here is that we understand the nature of LiveData:

  • LiveData is essentially an Observable — which can be observed/waited
  • Transformations.map observes a LiveData and map it’s value to another data — so we used to map from action to result
  • Transformations.switchMap observes a LiveData and map it to another LiveData (which can be observed/waited — perfectly suitable for async waiting)

Deeper into implementation of Handler and Reducer

Let’s get into detail of the handler:

The secret of handle function (line 3–7) is liveData coroutine (detail here) which allow us to execute suspend code for async operations, emit multiple results… without blocking main thread.

Meanwhile reduce function (line 11–16) is more simple, it just processes data synchronously and of course, ensures immutable state.

As you see, both handle and reduce functions will just pass control to productListViewModel (actually the name is not correct, it’s not a ViewModal but just a class with handle and reduce functions). It’s to break down the processing work so ActivityViewModel will not grow to thousand lines of code when the project evolves.

The code of ProductListViewModel:

handle is a suspend function, which is extension of LiveDataScope<ProductListResult>, thus it can emit results of type ProductListResult— in this example emit ProductListResult.Loading first to indicating loading UI (line 28), then call search API and emit ProductListResult.Success when API return (line 37) or ProductListResult.Failure when API error.

For actions that do not need async calls, we wrap them and forward to reducer directly with ProductListResult.ActionWrapper (line 4).

reduce is written as extension of ProductListViewState which brings a little more readable (love Kotlin :))

A single ViewState for all Fragments

When I first wrote ProductListViewModel, it was a real ViewModel, which is created when ProductListFragment is created. But a local ProductListViewState seems inadequate in some cases — for example when we navigate to the detail screen of the product (ProductDetailFragment), we would like to access some product data got from ProductListFragment before…

To achieve this “globalViewState, I moved ViewModel to Activity scope so it can be shared across Fragments — a simple implementation you can find here

Github project

References

My work uses materials from great articles and examples, so many thanks to the original authors :D

Thanks for reading!

--

--