Chapters

Hide chapters

Advanced Android App Architecture

First Edition · Android 9 · Kotlin 1.3 · Android Studio 3.2

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

17. Model View Intent Theory
Written by Aldo Olivares

You have learned about many different architecture patterns at this point, including MVVM, MVI and MVC.

In this chapter, you are going to learn about a very different architecture pattern for building Android apps called MVI.

Along the way, you will learn:

  • What MVI is and how it works.
  • The different layers of the MVI architecture pattern.
  • How a unidirectional flow of an Android app works.
  • How MVI improves the testability of your app by providing predictable and testable states.
  • MVI advantages and concerns vs other architecture patterns.

What is MVI?

MVI stands for Model-View-Intent. MVI is one of the newest architecture patterns for Android. The architecture was inspired by the unidirectional and cyclical nature of the Cycle.js framework and brought to the Android world by Hannes Dorfaman.

MVI works in a very different way compared to its distant relatives such as MVC, MVP or MVVM. The role of each of its components goes as follows:

  • Model: Represents a state. Models in MVI should be immutable to ensure a unidirectional data flow between them and the other layers in your architecture.
  • Intent: Represents an intention or a desire to perform an action by the user. For every user action, an intent will be received by the View, which will be observed by the Presenter and translated into a new state in your Models.
  • View: Just like in MVP they are represented by Interfaces, which are then implemented in one or more Activities or Fragments.

Next, you’ll explore each of the layers one by one.

Model

In other architecture patterns such as MVVM or MVP, the Models act as a rather simple layer to hold your data and act as a bridge to the backend of your app such as your databases or your APIs. However, in MVI, Models have a much more important role; they not only hold data, but also represent the state of your app.

But… What is the state of your app?

As you learned in the RxJava chapter, reactive programming is a paradigm in which you react to a change, such as the value of a variable or a button click in your UI. Well, when your apps react to this change, they enter into a new state. The new state is usually, but not always, represented as a UI change with something like a progress bar, a new list of movies or a completely different screen.

To illustrate how Models work in MVI, imagine that you want to retrieve a list of the most popular movies from a web service such as the IMDB. In an app built with the usual MVP pattern, Models are a rather simple class that represent the data in your app such as this:

data class Movie(
  var voteCount: Int? = null,
  var id: Int? = null,
  var video: Boolean? = null,
  var voteAverage: Float? = null,
  var title: String? = null,
  var popularity: Float? = null,
  var posterPath: String? = null,
  var originalLanguage: String? = null,
  var originalTitle: String? = null,
  var genreIds: List<Int>? = null,
  var backdropPath: String? = null,
  var adult: Boolean? = null,
  var overview: String? = null,
  var releaseDate: String? = null
)

Then, the Presenter would be in charge of using the above-mentioned Model to display a list of movies with code like this:

class MainPresenter(private var view: MainContract.View?) : MainContract.Presenter, MainContract.InteractorOutput {

  override fun onViewCreated() {
    view.showLoading()
    interactor
        .loadMovieList()
        .observe(view as MainActivity, Observer) {movieList ->
          movieList.let {
            this.onQuerySuccess(movieList)
          }
        }
  }

  override fun onQuerySuccess(data: List<Movie>) {
    view.hideLoading()
    view.displayMovieList(data)
  }
}

On the other hand, with an app built with the MVVM architecture pattern, something similar happens. The difference is that, instead of the Presenter, your ViewModel uses RxJava or LiveData to bind Observables to your UIs and display the data contained in your Models.

While this above approach is not bad, there are still a couple of issues that MVI attempts to solve:

  • Multiple Inputs: In MVP and MVVM the Presenter and the ViewModel usually end up with a large number of inputs and outputs that have to be managed very carefully. This becomes a huge problem on big apps with a large number of background tasks tied to multiple Observables.
  • Multiple States: With patterns such as MVP or MVVM, the business logic and the Views may have a different state at any point. You often synchronize the state with Observable/Observer callbacks but this may lead to a conflicting behavior if the synchronization is not handled properly. It becomes difficult to decide which is the correct state of your app at any given point… Should I display a progress bar? Should I display a list of movies? What should I do?

How do you solve the above issues? By making your Models represent a state rather than plain old data.

This is how you could create a Model that represents a state from the previous example:

sealed class MovieState {
  object LoadingState : MovieState()
  data class DataState(val data: List<Movie>) : MovieState()
  data class ErrorState(val data: String) : MovieState()
  data class ConfirmationState(val movie: Movie) : MovieState()
  object FinishState : MovieState()
}

When you model your Models like this, you no longer have to manage the state in multiple places such as your Views and the Presenters/ViewModel. They will indicate when your app should display a progress bar, an error message or a list of items.

Then, the Presenter for the above example would look like this:

class MainPresenter(private var view: MainContract.View?) : MainContract.Presenter, MainContract.InteractorOutput {

  override fun onViewCreated() {
    view.render(MovieModel(true, null, null))
    interactor
        .loadMovieList()
        .observe(view as MainActivity, Observer) { movieList ->
          movieList.let {
            this.onQuerySuccess(movieList)
          }
        }
  }

  override fun onQuerySuccess(data: List<Movie>) {
    view.render(MovieModel(false, data, null))
  }
  private fun observeMovieDisplay() = movieInteractor.getMovieList()
      .observeOn(AndroidSchedulers.mainThread())
      .doOnSubscribe { view.render(MovieState.LoadingState) }
      .doOnNext { view.render(it) }
      .subscribe()
}

Your Presenter now only has one output: the state of your View. This is done with the View’s render() method that accepts as an argument the current state for your app.

Another important and distinctive characteristic of the Models in MVI is that they should be immutable to maintain your business logic as the single source of truth.

This way, you are sure that your Models won’t be modified in multiple places thus maintaining a single state during the whole lifecycle of your app.

To make things clearer, imagine that you want to add a new item to a list of todo items. This is how a typical implementation of MVI will work under the hood:

  1. An Observable will send a notification to its subscribers about a new item being added to the list. Usually, this means adding a new record in a local and/or remote database.
  2. A method in your Presenter, such as presenter.addNewItem(item), will be called.
  3. Your business logic will use your current Model to create a new Model which contains the new set of items. It is very important that you remember the immutability of your Models at this point since you can’t just use the current Model to add the new item, you need to create a new one.
  4. Your Presenter will then be notified about a new state in your app with an Observer.
  5. Your Presenter will then call a render() method in your View and pass the new Model with the updated information as an argument.
  6. Your View will display the new list of items based on the Model received from the Presenter.

The interaction between the different layers can be illustrated by the following diagram:

Do you notice something in particular about this diagram? If you said cyclical flow, you are correct!

Thanks to the immutability of your Models, and the cyclical flow of your layers, you get other benefits:

  • Single State: Since immutable data structures are very easy to handle and can only be managed in one place, you can be sure there will only be a single state between all the layers in your app.
  • Thread Safety: This is specially useful while working with reactive apps that make use of libraries such as RxJava or LiveData. Since no methods can modify your Models they will always need to be recreated and kept in a single place, with this you make sure that there will be no other side effects such as different objects modifying your Models from different threads.

Of course the above are just hypothetical examples and you could model your Models and Presenters in a very different way, but the main premise is the same.

Now, take a look at the Views and Intents.

Views & Intents

In MVI, just like in in MVP, the Views are defined with the help of an Interface that acts as a contract which is implemented by a Fragment or an activity. The difference lies in the fact that Views in MVI tend to have a single render() method that accepts a state to render to the screen and different intent() methods as Observables that respond to user actions.

The intents in MVI don’t represent the usual android.content.Intent class that is used for things like starting a new class. Intents in MVI represent an action to be performed that is translated to a change of the state in your app. For this simple example you only have one intent, the getItemsIntent(), but you can have any number of intents in your Views depending on the number of actions.

This is how an Activity would implement the MainView interface from above:

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

  //1
  override fun getItemsIntent() = button.clicks()

  //2
  override fun render(state: ViewState) {
      when(state) {
          is ViewState.DataState -> renderDataState(state)
          is ViewState.LoadingState -> renderLoadingState()
          is ViewState.ErrorState -> renderErrorState(state)
      }
  }

  //4
  private fun renderDataState(dataState: ViewState.DataState) {
      //Render Data on Screen
  }

  //3
  private fun renderLoadingState() {
      //Render Loading indicator on screen
  }

  //5
  private fun renderErrorState(errorState: ViewState.ErrorState) {
      //Render Error on Screen
  }
}

Taking each commented section in turn:

  1. getItemsIntent(): Binds UI actions to the appropriate intents. In this case, we are binding a button click Observable to the getItemsIntent() method.
  2. render(): Maps your ViewState to the correct methods in your View.
  3. renderDataState() Renders the data contained in your Model to your View. This data can be anything such as weather data, a list of movies or an error.
  4. renderLoadingState(): Renders a loading screen in your View.
  5. renderErrorState(): Renders an error message in your View.

As you can see in the example above, you only have one render() method that receives the state of your app from your Presenter and an Intent that is triggered by a button click. This state is then translated into a UI change such as an error message or a loading screen.

State Reducers

With your usual mutable Models it is very easy to change the state of your app. Whenever you need to add, remove or update some underlying data you just need to call a method in your Models such as this:

myModel.addItems(newItems)

But you already learned that, since your Models are immutable, they need to be recreated each time the state of your app changes. If you just want new data to be displayed you can just create a new Model, but what do you do when you need information from a previous state?

This is where State Reducers come to save the day.

State Reducers are a concept derived from the reducer functions in reactive programming. Reducer functions, to put it simply, are functions that provide steps to properly merge things into a single component called the accumulator.

Since reducer functions are such a handy tool for developers most standard libraries have a similar method already implemented for their data structures. Kotlin’s Lists, for example, include a reduce() method that accumulates a value starting with the first element of the list and applying the operation passed as an argument:

val myList = listOf(1, 2, 3, 4, 5)
var result = myList.reduce { accumulator, currentValue ->
  println("accumulator = $accumulator, currentValue = $currentValue")
  accumulator + currentValue
}
println(result)

Output:

accumulator = 1, currentValue = 2
accumulator = 3, currentValue = 3
accumulator = 6, currentValue = 4
accumulator = 10, currentValue = 5
15

The above code iterates over each element of myList using the reduce method and adds each element to the current accumulator value.

Reducer functions basically consist of two main components:

  • Accumulator The total value accumulated so far in each iteration of your reducer function. It is usually the first argument.
  • Current Value The current value passing through each iteration of your reducer function. It is usually the second argument.

Easy right?

But what does all of this have to do with State Reducers and MVI?

Well, State Reducers work in a very similar way to reducer functions, the main difference lies in that State Reducers create a new state for your app based on a previous state and a current state that holds the new changes.

The overall process goes as follows:

  • You create a new state that represents the new changes in your app. This state is usually called PartialState.
  • When there is a new Intent that requires a previous state of your app as a starting point you will create a new PartialState rather than a complete state.
  • You create a new reduce() method that takes a previous state and a PartialState as arguments and defines how to merge both into a new state to be displayed.
  • You will then use the RxJava scan() method to apply your reduce() method to the initial state of your app and return the new state.

Naturally, it is up to each developer to implement a reducer function to properly merge two states of the current app. However, it is a common approach to use RxJava scan/merge operators to help with this task.

MVI Advantages and Concerns

Just like the previous patterns, Model-View-Intent is just an additional tool that you have at your disposal to create maintainable and scalable apps.

The main advantages of MVI are:

  • A unidirectional and cyclical data flow for your app.
  • Consistent state during the whole lifecycle of your Views.
  • Immutable Models that provide a reliable behavior and thread safety on big apps.

Probably the only downside of using MVI rather than other architecture patterns for Android is that the learning curve for this pattern tends to be a bit higher since you need to have a decent amount of knowledge of other intermediate/advanced topics such as reactive programming, multi-threading and RxJava. Therefore, other architecture patterns such as MVC or MVP might be easier to grasp for beginner Android developers.

Frequently Not Asked MVI Questions

Q. MVI and MVP look very similar…What is the main difference between the two patterns?

Both patterns rely on similar components such as Presenter, Views and Models. The main difference lies in the way those components are implemented and interact with each other in your app. For instance, the Models in MVI represent a state, rather than just data and the Views in MVI tend to have a single render() method that receives the state from the Presenter which is then mapped to the appropriate actions.**

Q. Is there an actual Intent layer?

It depends on what you mean by layer. As explained in this chapter, Intent in MVI represents an intention to do something like a database update or a web service call. You won’t typically find an Intent package or class in your MVI apps.

Q. Is it completely necessary to use RxJava in MVI?

A. No, it is not necessary to use a reactive programming library such as RxJava to create apps with the MVI architecture pattern. However, they will make your life much easier when you need to react to UI actions and observe for state changes in your Models.

Q. Has anyone actually asked you these questions before?

A. Nope, but someone might, and I want to be ready!

Key points

  • MVI stands for Model-View-Intent.

  • Models in MVI represent a state of your app.

  • The state represents how your app behaves or reacts at any given moment such as a loading screen, new data about to be displayed on a list or even a network error.

  • Views in MVI can have one or more intent() methods that handle user actions and a single render() method that renders the state of your app.

  • The Intent represents an intention to perform an action by the user like an API call or a new query in your database. It does NOT represent the usual android.content.Intent.

  • Reducer functions are functions that provide steps to properly merge things into a single component called the accumulator.

  • MVI provides a unidirectional and cyclical data flow for your app.

  • MVI relies on intermediate/advanced android topics such as reactive programming, multi-threading and RxJava. Therefore, it might be harder to learn for beginner developers compared to other patterns such as MVC or MVP.

Where to go from here?

MVI is a powerful architecture pattern that relies on a unidirectional data flow and immutable Models to solve common concerns across Android development such as the state problem and thread safety.

Since understanding RxJava is a very important prerequisite to MVI you might want to take a look at the RxJava chapters if you haven’t done so already. Also, make sure to checkout the MVP Theory and MVP Sample chapters if you haven’t used that pattern in the past.

In the next chapter, you will apply your newly acquired knowledge to rewrite the WeWatch app to use the MVI architecture pattern.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.