Modern Design patterns in android — MVI and the Unidirectional Data Flow paradigm

Ziv Kesten
9 min readJun 8, 2020

--

Over the past few years, the android framework placed a big emphasis on providing developers with components for creating more efficient design patterns. the MVVM (Model — View — ViewModel) was chosen to be the preferred way to design an android system, and built-in components were provided by the framework so that developers won't have to deal with the headache of lifecycle or scoping.

After listening to Kaushik Gopal on the Android podcast, Fragmented, I became aware of the trend to implement Unidirectional Data Flow in android design, however, Gopal was introducing a design that was heavily on the RxJava side of the river.
While RxJava is a great and powerful framework, I imagine a lot of android developers may have a superficial knowledge of it and may get overwhelmed by its vast array of operators and concepts.

So I decided to build a small sample app using this MVI pattern, implementing only native android components — LiveData, and the android viewModel component.

But wait.. what the h**l is MVI?

MVI stands for Model-view-Intent and it is a design pattern that uses Unidirectional Data Flow to achieve something some people might know from the react world.

In simpler terms, it means that we have three main foundations to work with:

  1. model (this is the viewModel) It is a component in charge of representing a state of the view at any given moment.
  2. view which can be any piece of UI presented to the user and may hold a certain state. This can be an Activity, a Fragment or any other custom views that hold a mutable state.
  3. Intent which, not unlike the android intent, represents the intention or desire to perform an action by the user or by the system.

By using this design we simplify the responsibilities of each component and we can generify flow between the components

I don’t get it… show me some code

So to fully understand how to use this pattern let's get started with a simple app that shows us the latest headlines and then by tapping an item from the headlines list, we can view the specific article, the article screen would look something like this:

In this view, we have a backdrop which is the image shown,
We have a title text and a description text, these are all our mutable elements.
And so, to represent those elements, we will construct a ViewState class with those values.

data class ViewState(
val backDrop: String? = null, // A url to the article image
val title: String = "", // The title of the article
val description: String = "" // The description of the article
)

As can be seen, the ViewState class is a representation of all static elements on the screen, the backdrop for the image, the title, and the description for the texts, this is the viewModel output.

Now, on this screen, tapping on the floating action button might trigger an action, like adding it to our favorite articles, so once we do that we would want to display a Snackbar confirming our action was completed.

A Snackbar showing up is not a state, it is an effect, the way we determine what qualifies as a state versus what is an effect is simple:

State is any element on the screen which we would want to persist.
Effect is a one-time UI element that should not appear again on the next state update or if we rebuild the view (for example, after screen rotation).

Let’s look at our ViewEffect object then:

sealed class ViewEffect {
data class NavigateToLink(val url: String?) : ViewEffect()
data class ShowSnackBar(val messageResource: Int) : ViewEffect()
}

Notice we are using a sealed class here, if you ever worked with Swift you would know the term associated values, what this basically means is that you can have a class that functions like an enum, but could hold values as well as the enum cases.

Kotlin provides this ability via the sealed modifier, and, in our case, it would provide a very convenient way to declare a restricted set of types with values associated with them.
We have two types of view effects in our sample: ViewEffect.NavigateToLink which holds a value of the URL to navigate to.
And ViewEffect.ShowSnackBar which will hold the value of the message we want to show in the SnackBar.

Now that we have our building blocks ViewState and ViewEffect, let's see how we communicate them between the UI component (The screen) and the business logic component (The viewModel).

The UI has two different general types of events it can forward to the viewModel:
1. User inputs: Button clicks, screen swipes, etc’
2. System change inputs: screen loads, lifecycle events, deep linking, etc.
So let's have a look at our Event class:

sealed class Event {
object AddToFavouritesClicked : Event()
object LinkClicked : Event()
object SwipeToRefreshEvent: Event()
object ScreenLoad: Event()
data class DataReceived(val article: Article?) : Event()
}

Looking at this class we have three user inputs: AddToFavouritesClicked, LinkClicked and SwipeToRefreshEvent.
As well as two system events: ScreenLoad and DataReceived (which holds the value extracted from the intent).

So we have events coming from the UI, and we have a viewState and viewEffect going back to the UI.

how do we connect all those pieces?

ViewModel here to connect the dots!

The Android framework viewModel is a very powerful component that can manage its own lifecycle, holds no reference to any UI component, and persists over UI state changes.
And we will use it as the glue to paste together our system events, let's have a look at what the viewModel of the article, view looks like.

break it down now.

The first part is the data the viewModel is holding, and that represents the state of the UI.

private var data: Article? = null

In our example, this is an Article object, But this can be anything from primitive values to complex data structures.

Next, we have two liveData objects that represent the two outputs of the viewModel: the viewState liveData and the viewEffect liveData.
If you are not familiar with liveData, you can think of it as a stream of data (much like an Rx observable) that is geared towards UI data changes.

private val viewState = MutableLiveData<DetailViewState>()

private val viewAction = MutableLiveData<ViewEffect>()

It is important to note that the liveData objects should be private, this is since we don’t want anyone from outside the viewModel to post values on their streams, the way those liveData objects can be observed and consumed by outside components are through the next two lines.

val obtainStateDetail: LiveData<DetailViewState> = viewState

val obtainAction: LiveData<ViewEffect> = viewAction

This is what the UI component would observe to receive a stream of state changes.

The next property is the currentViewState.

private var currentViewState = DetailViewState()
set(value) {
viewState.value = value
field = value
}

This property holds the last known state of the UI and we add a set method to it so that we can kill two birds with one stone (Don’t kill birds, birds are generally ok).
When we declare this set method we provide two actions.

  1. field -> value would set the provided values in the viewState.
  2. viewState.value -> value would update the liveData stream and subsequently notify its observers (we’ll get to them when we see the UI code.

Now that we have declared all our variables, we just need to take an event and transform it into a state or an effect, this is the Intent part of the MVI design, an action, input, or any other change that would mutate our state in any way.
an intention.

Look how simple this is!
We call when on the event (Similar to switch for java or swift developer) and for each event type we determine the action to be taken with it.
Remember that Event is a sealed Kotlin class and it allows us to get an event with a value associated with it, for example, we can look at the event for adding an article to the favorites:

So if the user taps the plus button on the screen, we post a value to the liveData stream that will display a SnackBar with the message provided.
This is where we can send a network request, manipulate data in our local database, or do anything we can think about, we can use coroutines for that, and there are a lot of articles out there about how to use coroutines and suspend methods, I encourage you to learn more about this fascinating subject.
but for our simple example, we simply post a success result to the viewAction liveData stream with the viewEffect of ShowSnackBar

The screen as an event emitter and state renderer

So we went through the model part of the MVI (viewModel) and we have seen the Intent that is forwarded to it as an event.
Now it's time to take a look at the view part, let’s see the article screen code.

The first thing we need to look at is:

private val viewModel by sharedViewModel<ArticleViewModel>()

This is the viewModel provided to us by the android viewModel injection, the sharedViewModel is an extension function of fragment and allows us to share a viewModel between screens, if we want to, if you are not familiar with the android MVVM pattern, you can read all about it here.

In onCreateView, we set the click listeners and map them to viewModel events.

private fun setClickEvents() {
binding.link.setOnClickListener {
viewModel.event(Event.LinkClicked)
}

binding.fab.setOnClickListener {
viewModel.event(Event.AddToFavouritesClicked)
}
}

Pay attention to the fact that the view does absolutely nothing with the tap events other than forwarding them to the model as events object that may or may not contain some values, on the viewModel side, they would be mapped to effects or states.
So we send the input to the viewModel, now what?

on onViewCreated we set up our way to listen to the changes in state-reported back to us from the viewModel.

private fun observeViewEffects() {
viewModel.obtainAction.observe(viewLifecycleOwner, Observer {
trigger(it)
})
}

private fun observeViewState() {
viewModel.obtainStateDetail.observe(viewLifecycleOwner, Observer {
renderState(it)
})
}

It's worth taking a second and appreciating the simplicity of this code, on a large scale this could stay as simple as this.

So what is happening here?

We call the viewModel’s obtainAction and get back a liveData stream to observe, whenever the viewModel posts a new state to this stream, it will end up in this block of code where we can render the new state by calling renderState() passing in the new ViewState object.

One important thing to mention but not to dive too deeply into is viewLifeCycleOwner that is passed to the observing block. it indicates the scope in which this observation should take place, this is done to avoid cases where the UI element observing the live data is destroyed for some reason and we are still holding a reference to it, it prevents memory leaks and illegal state exceptions, read more about scopes and lifecycle in android here.

The same way we observe state and call renderState(), we can do for the viewEffect, we just observe it and call trigger() to trigger the desired effect.
If the screen is rebuilt for any reason, the viewEffect UI action would not be called again.

So to wrap it all up

We have seen how we can use the MVI to create simple, understandable, scalable code that relies on a unidirectional data flow and immutable Models to address various problems in the android world related to state management.

  1. Model that represents state and converts inputs into immutable state objects and one-time UI effects.
  2. View that forwards input or system changes to the model.
  3. Intent is the intention to start an action or a process.

All the code for this example can be seen on my GitHub repository.

This article was inspired by Kaushik Gopal’s repo and podcast and can be viewed here.

--

--

Ziv Kesten
Ziv Kesten

Written by Ziv Kesten

I am a mobile developer, an android enthusiast and a drone lover (Secretly, don’t tell the wife)

Responses (1)