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. By Aldo Olivares.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
MVI Architecture for Android Tutorial: Getting Started
20 mins
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.