Android & Kotlin Tutorials

Learn Android development in Kotlin, from beginner to advanced.

MVI Architecture for Android Tutorial: Getting Started

Learn about the MVI (Model-View-Intent) architecture pattern and prepare to apply it to your next Android app.

4.2/5 10 Ratings

Version

  • Kotlin 1.3, Android 4.1, Android Studio 3

Developers can choose from several architecture patterns to create scalable and maintainable apps like MVC, MVP and MVVM. All of them use the imperative programming developers know and love. In this tutorial, you’ll learn about a very different architecture pattern. MVI uses reactive programming to build Android apps.

This tutorial covers:

  • What MVI is and how it works.
  • The 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.
  • Advantages and disadvantages of using MVI compared to other architecture patterns.

Ready to get started?

What is MVI?

MVI stands for Model-View-Intent. MVI is one of the newest architecture patterns for Android, inspired by the unidirectional and cyclical nature of the Cycle.js framework.

MVI works in a very different way compared to its distant relatives, MVC, MVP or MVVM. The role of each MVI components is 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.
  • Like in MVP, Interfaces in MVI represent Views, which are then implemented in one or more Activities or Fragments.
  • Intent represents an intention or a desire to perform an action, either by the user or the app itself. For every action, a View receives an Intent. The Presenter observes the Intent, and Models translate it into a new state.

It’s time for an in-depth exploration of each layer.

Models

Other architecture patterns implement Models as a layer to hold data and act as a bridge to the backend of an app such as databases or APIs. However, in MVI, Models both hold data and represent the state of the app.

What is the state of the app?

In reactive programming, an app reacts to a change, such as the value of a variable or a button click in your UI. When an app reacts to a change, it transitions to a new state. The new state may appear as a UI change with a progress bar, a new list of movies or a different screen.

To illustrate how Models work in MVI, imagine you want to retrieve a list of the most popular movies from a web service such as the TMDB API. In an app built with the usual MVP pattern, Models are a class representing data like 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
)

In this case, the Presenter is in charge of using the Model above to display a list of movies with code like this:

class MainPresenter(private var view: MainView?) {    
  override fun onViewCreated() {
    view.showLoading()
    loadMovieList { movieList ->
      movieList.let {
        this.onQuerySuccess(movieList)
      }
    }
  }
  
  override fun onQuerySuccess(data: List<Movie>) {
    view.hideLoading()
    view.displayMovieList(data)
  }
}

While this 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 often end up with a large number of inputs and outputs to manage. This is problematic in big apps with many background tasks.
  • Multiple states: In MVP and MVVM, the business logic and the Views may have different states at any point. Developers often synchronize the state with Observable and Observer callbacks, but this may lead to conflicting behavior.

To solve this issues, make your Models represent a state rather than data.

Using the previous example, this is how you could create a Model that represents a state:

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 create Models this way, you no longer have to manage the state in multiple places such as in the views, presenters or ViewModel. The Model 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 val compositeDisposable = CompositeDisposable()
  private lateinit var view: MainView

  fun bind(view: MainView) {
    this.view = view
    compositeDisposable.add(observeMovieDeleteIntent())
    compositeDisposable.add(observeMovieDisplay())
  }
  
  fun unbind() {
    if (!compositeDisposable.isDisposed) {
      compositeDisposable.dispose()
    }
  }
  
  private fun observeMovieDisplay() = loadMovieList()
      .observeOn(AndroidSchedulers.mainThread())
      .doOnSubscribe { view.render(MovieState.LoadingState) }
      .doOnNext { view.render(it) }
      .subscribe()
}

Your Presenter now has one output: the state of the View. This is done with the View’s render(), which accepts the current state of the app as an argument.

Another distinctive characteristic of models in MVI is that they should be immutable to maintain your business logic as the single source of truth. This way, you’re sure that your Models wont be modified in multiple places. They’ll maintain a single state during the whole lifecycle of the app.

The following diagram illustrates the interaction between the different layers:

Do you notice anything in particular about this diagram? If you said cyclical flow, you’re 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 must be managed in one place, you can be sure there will be a single state between all the layers in your app.
  • Thread Safety: This is especially 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. This protects against side effects such as different objects modifying your Models from different threads.

These examples are hypothetical. You could construct your Models and Presenters differently, but the main premise would be the same.

Next, take a look at the Views and Intents.

Views and Intents

Like with MVP, MVI defines an interface for the View, acting as a contract generally implemented by a Fragment or an Activity. Views in MVI tend to have a single render() that accepts a state to render to the screen. Views in MVI use Observable intent()s to respond to user actions. MVP on the other hand generally uses verbose method names to define different inputs and outputs.

Note: Intents in MVI don’t represent the usual android.content.Intent class used for things like starting a new class. Intents in MVI represent a future action that changes the app’s state.

This is how a View in MVI might look:

class MainActivity : MainView {
  
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
  }
    
  //1
  override fun displayMoviesIntent() = button.clicks()
    
  //2
  override fun render(state: MovieState) {
    when(state) {
      is MovieState.DataState -> renderDataState(state)
      is MovieState.LoadingState -> renderLoadingState()
      is MovieState.ErrorState -> renderErrorState(state)
    }
  }
    
  //4
  private fun renderDataState(dataState: MovieState.DataState) {
      //Render movie list
  }
    
  //3
  private fun renderLoadingState() {
      //Render progress bar on screen
  }
	
  //5
  private fun renderErrorState(errorState: MovieState.ErrorState) {
      //Display error mesage
  }
}

Taking each section in turn:

  1. displayMoviesIntent: Binds UI actions to the appropriate intents. In this case, it binds a button click Observable as an intent. This would be defined as part of your MainView.

    Note: We’re using RxBinding to convert button click listeners into RxJava Observables.

  2. render: Map your ViewState to the correct methods in your View. This would also be defined as part of your MainView.
  3. renderDataState: Render the Model data to the View. This data can be anything such as weather data, a list of movies or an error. This is generally defined as an internal method for updating the display based on the state.
  4. renderLoadingState: Render a loading screen in your View.
  5. renderErrorState: Render an error message in your View.

The example above demonstrates how one render() receives the state of your app from your Presenter and an Intent triggered by a button click. The result is a UI change such as an error message or a loading screen.

State Reducers

With mutable Models, it’s easy to change the state of your app. To add, remove or update some underlying data, call a method in your Models such as this:

myModel.insert(items)

You know that Models are immutable, so you have to recreate them each time the state of your app changes. If you want to display new data, create a new Model. What do you do when you need information from a previous state?

The answer: State reducers.

The concept of State Reducers derives from Reducer Functions in reactive programming. Reducer functions provide steps to merge things into the accumulator component.

Reducer functions are a handy tool for developers, and most standard libraries have similar methods already implemented for their immutable data structures. Kotlin’s Lists, for example, include reduce(), which accumulates a value starting with the first element of the list, 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)

Running the above code would produce the following output:

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

Note: You can run the above snippet of code in an online Kotlin REPL. The author of this tutorial recommends this one. Its a quick and fun way to test small pieces of code when you don’t want to boot up a whole IDE.

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

Reducer functions consist of two main components:

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

What does this have to do with State Reducers and MVI?

Tying It All Together

State reducers work similarly to reducer functions. The main difference is that State Reducers create a new state for your app based on a previous state, as well a current state that holds the new changes whereas reducer functions generally operate on the collection itself.

The process works as follows:

  • Create a new state called PartialState that represents new changes in your app.
  • When there is a new Intent that requires a previous state of your app as a starting point, create a new PartialState rather than a complete state.
  • Create a new reduce() function that takes the previous state and a PartialState as arguments and defines how to merge both into a new state to be displayed.
  • Use RxJava scan() to apply reduce() to the initial state of your app and return the new state.

It’s up to each developer to implement a reducer function to merge the two states of the current app. Developers often use RxJava scan or merge operators to help with this task.

MVI: Advantages and Disadvantages

Model-View-Intent is a tool to create maintainable and scalable apps.

The main advantages of MVI are:

  • A unidirectional and cyclical data flow.
  • A consistent state during the lifecycle of Views.
  • Immutable Models that provide reliable behavior and thread safety on big apps.

One downside of using MVI rather than other architecture patterns for Android is that the learning curve for this pattern tends to be a bit longer. You need a decent amount of knowledge of other intermediate and advanced topics such as reactive programming, multi-threading and RxJava. Architecture patterns such as MVC or MVP might be easier to grasp for new Android developers.

Additional MVI Questions

Q. MVI and MVP look similar. What is the main difference between them?

A. Both patterns rely on similar components such as Presenter, Views and Models. The main difference lies in the implementation of the components and their interactions in an app. For instance, Models in MVI represent a state, and MVP represents data.

Views in MVI tend to have a single render() method that receives the state from the Presenter, which is mapped to the appropriate actions.

Also, MVP tends to be implemented with traditional, imperative programming while MVI is reactive by nature.

Q. Is there an actual Intent layer?

A. It depends. Intents in MVI represent an intention to do something like a database update or a web service call. You won’t always find an Intent package or class in MVI apps.

Q. Is it necessary to use RxJava in MVI?

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

Q. Has anyone asked you these questions before?

A. No, but someone might, and I want to be ready. : ]

Where to Go From Here?

In this tutorial, you learned the key points of MVI, including:

  • MVI stands for Model-View-Intent
  • Models in MVI represent a state of an app.
  • A state represents how an app behaves or reacts at any given moment such when loading screen or displaying new data in a list or a network error.
  • Views in MVI can have one or more intent()s that handle user actions and a single render() 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 provide steps to merge things into a single component called the accumulator.
  • MVI provides a unidirectional and cyclical data flow.
  • In summary, MVI is a powerful architecture pattern. It relies on a unidirectional data flow and immutable Models to solve common concerns across Android development such as the state problem and thread safety.

    To learn more about RxJava, check out the official ReactiveX website for the documentation and many useful examples.

    I also recommend our Advanced Android app Architecture book, which contains three full chapters about MVI as well as other popular architecture patterns such as MVC, MVP, MVVM and VIPER.

    Finally, we also have video courses on MVP, MVVM, and MVI. The video courses build a complete app using each of the different patterns.

    Feel free to share your feedback or ask any questions in the comments below or in the forums!

Average Rating

4.2/5

Add a rating for this content

10 ratings

Contributors

Comments