Skip to content

Understanding How and Why: The MVI Android Architecture

MVP and MVVM are both pretty prominent, and nowadays, quite standard in the Android community but one other popular architecture has made a name for itself, and that’s MVI.

MVI stands for Model-View-Intent. Intent in this case isn’t the Android Intent used to start activities, no. Rather, it stands for an intention, a use-case, basically a desire to perform an action.

What’s interesting about MVI is that there’s no actual “Intent layer”. In fact, presenters are still used. Where does this differ from MVP then?

The Problem

In MVP, you can have any number of asynchronous streams (via RxJava, Coroutines, etc.) and when each completes, it calls the function on the View to make the corresponding UI change to the data just received. E.g. your app finished fetching all the data of the user’s profile, so it hides the progress bar and updates the UI with the user’s data.

Now imagine having many of these in one activity. Just imagine the sheer number of conflicts you can have that can lead to bugs in the UI, or how we MVI practitioners would like to call it, the View misinterpreting the state.

Look at the progress bar alone. This one little spinny thing is something you have to keep track of in every single data stream you execute to make sure it only hides itself when all is done.

These are some of the problems that MVI fixes. In MVI, you only pass a single object to the View EVER (Single Source of Truth baby). This is something we’d call a ‘State’.

Implementation

UserState.kt

sealed class UserState {
    object LoadingState: UserState()
    data class DataState(val data: List<User>): UserState()
    data class ErrorState(val error: String): UserState()
}

The State is a sealed class that represents the different possible representations of the View. In each state, you want to include all the data the View needs to display what it needs to display, because remember: this is the single source of truth for the View.

Speaking of the View, here’s what it may look like in the MVI architecture.

UsersActivity.kt

/**
 * An activity that displays a list of users
 */
class UsersActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun render(state: UserState) {
        when(state) {
            is UserState.LoadingState -> renderLoadingState()
            is UserState.DataState -> renderDataState(state)
            is UserState.ErrorState -> renderErrorState(state)
        }
    }

    private fun renderLoadingState() {
        // Render progress bar
    }

    private fun renderDataState(dataState: UserState.DataState) {
        // Render list of users
    }

    private fun renderErrorState(errorState: UserState.ErrorState) {
        // Render error message
    }

}

Only the render() method will be called from outside the View, and that will serve the current state of the activity. Based on what state is actually received, the View will decide how to render the activity.

Other than this, everything remains largely the same as with MVP. You still have model classes to represent the data you’re getting (which you pass into the DataState), and you still have a presenter that fetches this data then delegates work back to the View once it has the data. The main difference is instead of calling multiple different methods in the View for different sets of data, you always call render(), and you do so after updating the state.

So let’s look at how this works in code.

User.kt

data class User(
    val name: String,
    val email: String,
    val phone: String
)

Just for the record, the model classes themselves in MVI are pretty much the same they’d be anywhere.

UsersPresenter.kt

class UsersPresenter {

    private lateinit var view: UsersActivity
    private val scope = CoroutineScope(Job())
    var currentState: UserState? = null

    private val usersRepository = UsersRepository()

    fun bind(view: UsersActivity) {
        this.view = view
    }

    fun unbind() {
        scope.cancel()
    }

    private fun getUsers() {
        scope.launch {
            val usersList = loadUsers()
            view.render(reduce(currentState, usersList))
        }
    }

    private suspend fun loadUsers(): List<User> {
        return usersRepository.loadUsers()
    }

    private fun reduce(previous: UserState?, new: List<User>): UserState {
        if (previous != null && previous is UserState.DataState) {
            val usersMutable = ArrayList(previous.data)
            usersMutable.addAll(new)
            this.currentState = UserState.DataState(usersMutable.toList())
        } else {
            this.currentState = UserState.DataState(new)
        }

        return this.currentState!!
    }

}

In a nutshell, what’s happening here is we’re keeping track of the current state of the activity, and every time we receive new data, we get the view to render the data by passing it the new state.

What? What does reduce do? Sharp-eyed you must be,young Padawan. This is the final key component to the MVI architecture.

State Reducers

Within our state object, we keep all our models immutable, as they should be, but the question still stands: how do we update these models when new data comes in? After all, the goal of MVI was to more gracefully handle multiple streams of data right?

That’s where state reducers come in. State reducers derive from the concept of reducer functions, a type of function used for merging data through an accumulator.

Kotlin’s list has a reduce function built into it.

/**
 * Accumulates value starting with the first element and applying [operation] from left to right to current accumulator value and each element.
 * 
 * @sample samples.collections.Collections.Aggregates.reduce
 */
public inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {
    val iterator = this.iterator()
    if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
    var accumulator: S = iterator.next()
    while (iterator.hasNext()) {
        accumulator = operation(accumulator, iterator.next())
    }
    return accumulator
}

And what this does is converts a list into a single object of the list type by iterating through each item and applying a function to it.

private fun reduceNumbers(): Int {
    val oddList = listOf(1, 3, 5, 7, 9)
    return oddList.reduce { acc, i -> 
        acc + i
    }
}

State reducers in MVI follow the same concept. They’re functions that determine how to append new data to data currently being held.

Get the Code

I hope this tutorial was helpful, but if my explanation wasn’t worthy enough, then you can check out the source code here on Github.

You can also check out my tutorial on MVVM here.

As always, happy coding ༼ つ ◕_◕ ༽つ