MVI — another member of the MV* band

Every Android developer has heard about MVP and MVVM presentation patterns. Therefore, without even trying, we can easily explain the meaning of the “V,” “P” and “VM” in these acronyms. “V,” as in View, represents everything visible on the screen and Presenter (“P”) and ViewModel (“VM”) define either the presentation logic or how View is modeled. However, have you ever thought about the purpose of the “M,” which stands for Model?

If you google it, the most common explanations for Model are:

  • something which is responsible for handling business logic,
  • a place where your business logic and application data are stored,
  • an interface responsible for data management,
  • a representation of the data and business logic of your app.

In general, this can all be explained in one sentence: Model should serve as a gateway to your domain layer or business logic.

Can you spot how the definition of Model differs from View, Presenter and ViewModel? Why is Model called “Model” if it represents business logic? Why is it not called “Business” or “Logic” or something equally self-explanatory like the rest of the components?

MVIarticleGIF

To answer this question, let’s look at how Model was originally defined…

The concept of Model is nothing new. It was first defined by Trygve Reenskaugin 1979 as a part of MVC architecture.

“Model is responsible for representing state, structure, and behaviour of the user’s mental model.”

“A View presents information that it retrieves from one or more model objects.”

“Let the View register with the Model as being a dependent of the Model, and let the Model send appropriate messages to its dependents whenever it changes.

Loosely speaking, the original explanation specifies Model as an entity which tells View what to display on the screen. If Model changes, View gets notified about the change, and it will render the change on the screen.

And that’s where MVI comes in…

Untitled-design--1-

Imagine a classic conversation between two people, where one doesn’t interrupt the other. Each person listens and then reacts to what they heard.

What would we have to change if we want to replace one person with a computer? To allow for human-computer interaction, we need to add an interface, which is usually a keyboard or a mouse on the user’s side and a monitor or a speaker on the computer’s side.

Instead of speaking, a user will make an output by clicking on the mouse or keyboard, which is an input for the computer. The computer then processes the information, and it will produce an output by displaying information on the screen, which is an input for the user, and based on this input, the user can decide if he wants to start another circle or not.

2-1

In general, to link inputs with outputs, from a mathematical point of view, we need a function. As a result, we can replace each input-output pair with the function.

Graph of MVI architecture

Every component of this circle is a basic function, where its output becomes the next function’s input. There is only one direction all data can go. The result of user() is passed as an input value for intent(). It can’t go in the opposite nor, for that matter, in any other direction. The same rule is valid for the rest of the steps in the circle. The output of intent() is an input for model(). A new state (which will be defined later), as the only possible result of model(), is passed as an input for view(). And the output of view() is used to call user() again, and the circle just keeps going…

intent(user(view(model(intent(user())))))

However, we don’t code users. Therefore, our final function looks like this:

view(model(intent()))

Let’s assume that the function mentioned above is a pure function — a concept related to functional programming. Its main property is that it doesn’t have any side effects. Its output value is only determined by its input values without any observable side effects. Every time you run it with the same input values, you get the same output.

Consequently, we can say that the following three functions create the main components of MVI architecture:

  • Intent
  • Model
  • View

Intent

Intent is not the Android intent as we know it. Intent here means intention to change the state of our app by an intent. intent() can be started by the user clicking on a button or as a result of our API call that we want to display on the screen. All UI changes are a result of running intent(), whose triggers are usually defined in one file. This lets us have a clear understanding of what is going on in our app. Once we open this file, we can basically understand all our app’s use cases.

Side effects

Do you still remember the base graph of MVI, which is seemingly composed of pure functions? Well, this is not entirely true. If you think about it, every Android app is full of side effects like API calls, database operations or remote logging. All these actions are side effects, which pure functions technically forbid. What can we do about this?

There is a hidden component between intent() and model(), which handles side effects. The user dispatches intent(). A result of intent() is passed as an input value of model(). At the same time, intent() asks another component to trigger a side effect. A result of this side effect can be either nothing or a new intent(), which might run another side effect or could turn into an input value for model().

Handling side effects in MVI

Model

model() is a response to the creation of a new state, our model object, which tells View what to display on the screen. This can include everything from a progress bar to a list of fetched data to an error. Our View renders everything that the model/state contains.

data class CountryListViewState(
    val isLoading = false,
    val countries = emptyList(),
    val isRefreshing = true,
    val error = `UnknownHostException…`)

State

State is an immutable data structure. At any given moment, we have just one state in our app, which represents a single source of truth. The only way to change the state is to create a new one by triggering intent(). That’s why each UI change is a result of a run intent(). But when and how is the new state created? To understand this, let me introduce you to a new concept — Redux.

Redux

Redux is a predictable state container for JavaScript apps. It consists of three components:

  • State — state holder
  • Action — command to change the state
  • Reducer — a pure function that takes the previous state and action and creates a new state

Inside our model(), we call reducer() with a proper intent and the latest state and forward its result as an output value of model(). To be able to work with the latest state value, we can take advantage of RxJava’s scan operator, which applies a function (in our case reducer()) to each item sequentially emitted by an Observable. Then the operator emits each successive value.

It is important to remember that reducer() is a pure function — no surprises here, no side effects, just a new state calculation.

val reducer = BiFunction { previousState: CountryListViewState, result: CountryListResult ->
    when (result) {
        is LoadCountriesResult ->
            when (result) {
                is LoadCountriesResult.Success -> {
                    previousState.copy(
                        isLoading = false,
                        isRefreshing = false,
                        countries = result.countries
                    )
                }
                is LoadCountriesResult.Failure -> {
                    previousState.copy(
                        isLoading = false,
                        error = result.error
                    )
                }
                is LoadCountriesResult.InProgress -> {
                    if (result.isRefreshing) {
                        previousState.copy(
                            isLoading = false,
                            isRefreshing = true
                        )
                    } else previousState.copy(
                        isLoading = true,
                        isRefreshing = false
                    ) 
                }
            }
    } 
}

I really recommend reading the official documentation of Redux for JavaScript. It is really nicely written, and it will help you to understand the topic better.

State problem

One may ask why would we need a state? The reason is simple— to avoid a state problem.


class MyProfileViewModel {

    val progress = MutableLiveData()
    val myProfile = MutableLiveData()

    override fun loadProfile() {
        progress.value = true

        subscriptions.add(
            myProfileInteractor.getLoggedUser().subscribe(
                object : Subscriber<UserEntity>() {
                    override fun onCompleted() {
                        progress.value = false
                    }

                    override fun onError(e: Throwable) {
                        handleError(e)
                    }

                    override fun onNext(userEntity: UserEntity) {
                        userInteractor.setUser(userEntity)
                        myProfile.value = userEntity
                    }
                }
            )
        )
    }

    override fun handleEditProfileClick() {
        /***/
    }
}

It is common practice to hold a state for each layer of our app. Business logic has its own state, ViewModel has its own state, and theoretically, a view can also have its own state, if we define any kind of logic inside of an XML layout. With so many states to manage, we can easily reach a point where we no longer understand what is happening in our app, because we have lost control over all its states.

Moreover, a lack of proper state management can easily lead to a conflict state among our view, ViewModel and activity. If we are lucky enough, this will just result just in virtual bugs that display, for example, a success and progress state at the same time, but if we are not lucky, we might get an irreproducible bug report, which is impossible to fix.

Displaying a conflict state

View

We wanted our state to be a model and our view to present information from it. view() is a function that takes a new state and defines the display logic inside of it.


What about config change?

You just display the latest state.

Navigation in MVI architecture is still an open question.

One approach is that navigation should be a part of our state. If we want to change the screen, we have to trigger a relevant intent(), pass the whole circle and then, based on the value of our state, we can change the screen.

Another way is to call our navigation as a result of a side effect, which sounds nice, but we should keep in mind that side effects are usually related to API calls, database operations or remote logging, and I wouldn’t recommend mixing these concepts together.

What I suggest is to call navigation directly. I think that our state should hold data that you want to display on the screen, not which screen we want to display. The screen itself should know which data to take from the state. And especially now, when we can take advantage of The Navigation Component from Android Jetpack, it just makes much more sense. At least for me.

Wrapping up

So that’s a brief introduction to the state-oriented MVI architecture. Using this architecture brings the following benefits:

  • No state problem anymore, because there is only one state for our app, which is a single source of truth.
  • Unidirectional data flow, which makes the logic of our app more predictable and easier to understand.
  • Immutability — as long as each output is an immutable object, we can take advantage of the benefits associated with immutability (thread safety or share-ability).
  • Debuggability — unidirectional data flow ensures that our app is easy to debug. Every time we pass our data from one component to another, we can log the current value of the outflow. Thus, when we get a bug report, we can see the state our app was in when the error was made, and we can even see the user’s intent under which this state was created.
  • Decoupled logic, because each component has its own responsibility.
  • Testability — all we have to do to write the unit test for our app is call a proper business method and check if we’ve got a proper state.

However, nothing is perfect, and there are some drawbacks, you should be aware of before using MVI:

  • A lot of boilerplate — each small UI change has to start with a user’s intent and then must pass the whole circle. Even with the easiest implementation, you have to create at least an intent and a state object for every action made in our app.
  • Complexity — there is a lot of logic inside which must be strictly followed, and there is a high probability that not everybody knows about it. This may cause problems especially when you need to expand your team as it will take more time for newcomers to get used to it.
  • Object creation, which is expensive. If too many objects are created, your heap memory can easily reach full capacity and then your garbage collector will be too busy. You should strike a balance between the structure and size of your app.
  • SingleLiveEvents. To create these with MVI architecture (for example displaying a toast message), you should have a state with showMessage = true attribute and render it by showing the toast. However, what if a config change comes into play? Then you should display the latest state, which would present the toast again. Although that’s correct behavior, it is not so user friendly. We have to create another state telling us not to display the message again. A couple of seconds after emitting the state with showMessage = true we have to create a new state with showMessage = false (e.g. by using the RxJava timer operator). It is not the most ideal solution, but that’s how it is usually done.
    Edit: You can also try another solution of Mariusz Dąbrowski described here.

This architecture is not something I made up by myself. There are many libraries you can check out that are based on the state principle developed and used by well-known enterprises:

spotify/mobius

airbnb/MvRx

Tinder/StateMachine

badoo/MVICore

freeletics/RxRedux

A smart person once said:

If you like the code you wrote a year ago, you haven’t learned enough this year.“

I am not forcing you to use this architecture. Architecture is an evolution. There are always articles or videos popping up about a new architectonic principle. I just wanted to show you another way it can be used. Think about its benefits and drawbacks before deciding whether to use it in your next project.

Here’s the link to my MVI app (highly inspired by Benoit Quenaudon’s example)

And here’s the video of the related talk (thanks Barcelona Android Developer Group for providing the video):

I’d like to thank Alexander Kovalenko, Lubos Mudrak and Jirka Helmich for reviewing and improving this article with their observations!


Resources:

We're hiring

Share Article
Iveta Jurcikova

Iveta Jurcikova

I am a Slovak living in Prague, working as an Android developer in STRV. I am passionate about coding and love creating exciting projects for mobile.

You might also like...