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
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.
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:
Note: We’re using RxBinding to convert button click listeners into RxJava Observables.
-
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.
-
render: Map your ViewState to the correct methods in your View. This would also be defined as part of your
MainView
. - 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.
- renderLoadingState: Render a loading screen in your View.
- renderErrorState: Render an error message in your View.
Note: We’re using RxBinding to convert button click listeners into RxJava Observables.
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.
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 applyreduce()
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.