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

Image for post
Image for post

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 wont 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 and activity, a fragment or any other custom view the holds 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 lets 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:

Image for post
Image for post

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 represents 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 and the title and description for the texts, this is the viewModel output.

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

Image for post
Image for post

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 for 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, lets see how we communicate them between the UI component (The screen) and the business logic component (The viewModel).

Image for post
Image for post

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, deeplinking etc’
So lets 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, so 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, lets have a look at how the viewModel of the article view looks like.

Lets 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 represents 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 like a stream of data (much like an Rx observable) which 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 to 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 to 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 to it, for example, we can look at the event for adding an article to the favourites:

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 data base or anything we can think about, we can use coroutines for that, and there are alot 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 its time to take a look at the view part, lets see the article screen code.

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 an 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 then forwarding them to the model as events objects 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 setup 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)
})
}

Its worth to take a second and appreciate 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 to deeply in is viewLifeCycleOwner that is passed to the observing block. it indicate 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 reference to it, this is done to 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 that 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.

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

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