Android MVI-Reactive Architecture Pattern

Abhishek Srivastava
12 min readAug 16, 2020

--

If you are already aware of basic principles of architecture patterns and MVVM and MVI patterns in detail then skip the basics and jump to MVI + LiveData + ViewModel (or second) section of the article.

Architecture patterns are blueprint with a set of rules to follow. These patterns evolved through the mistakes done while coding over years. Below are the few architecture patterns widely used in Android.

  • Model View Controller(MVC)
  • Model View Presenter(MVP)
  • Model View View Model(MVVM)

The latest addition to these patterns is Model View Intent(MVI). To understand MVI, you should know how other patterns work.

Brief workflow of MVI:

User interaction with the UI is processed by business logic which brings change in the state. This new state is rendered on view and this newly updated view is shown to the user. This leads to a Unidirectional and Circular Data Flow.

A Quick Introduction to MVI

MVI is short for “Model, View, Intent”. Over the last year this architecture pattern received more and more attention among Android developers. It’s similar to other commonly known patterns like MVP or MVVM, but it introduces two new concepts: the intent and the state.
The intent is an event sent to the ViewModel by the View in order to perform a particular task. It can be triggered by the user or by other parts of your app. As a result of that, a new state is set on the ViewModel which in turn updates the user interface. In the MVI architecture, the View listens to the state. Every time the state changes, the View is notified.

So what is Model-View-Intent? Yet another architecture for user interfaces? Well, I recommend you to watch the following video about cycle.js presented by the inventor André Medeiros (Staltz) at JSConf Budapest in May 2015 (at least the first 10 minutes) to get a better understanding of the motivation behind MVI:

I know it’s javascript but the key fundamentals are the same for all UI based platforms, incl. android (just replace DOM with android layouts, UI widgets, …).

  • intent(): This function takes the input from the user (i.e. UI events, like click events) and translates it to “something” that will be passed as a parameter to model() function. This could be a simple string to set a value of the model to or more complex data structure like an Actions or Commands. Here in this blog post, we will stick with the word Action.
  • model(): The model function takes the output from intent() as input to manipulate the model. The output of this function is a new model (state changed). So it should not update an already existing model. We want immutability! We don’t change an already existing one. We copy the existing one and change the state (and afterward it can not be changed anymore). This function is the only piece of your code that is allowed to change a Model object. Then this new immutable Model is the output of this function.
  • view(): This method takes the model returned from model() function and gives it as input to the view() function. Then the view simply displays this model somehow.

But what about the cycle, one might ask? This is where reactive programming (RxJava, observer pattern) comes in.

So the view will generate “events” (observer pattern) that are passed to the intent() function again.

Sounds quite complex, I know, but once you are into it it’s not that hard anymore. Let’s say we want to build a simple android app to search github (rest api) for repositories matching a certain name. Something like this:

Why MVI?

As I mentioned above, patterns evolved through the mistakes done while coding over the years. Let us have a look at what are all the absolute advantages of MVI over other patterns, which gives us the answer to the question Why MVI?

Model as State: UI might have different states — Loading State, Data State, Error State, User Scroll Position etc. In MVI, models are formalized as the container of states which is not the case with other patterns. Every time a new immutable model is created which is then observed by the view. This way of creating an immutable model will ensure thread safety.

data class MviState(    
val isPageLoading: Boolean = false,
val isPullToRefresh: Boolean = false,
val isMoreRestaurantsLoading: Boolean = false,
val restaurantsObject: RestaurantsObject? = null,
val error: Throwable? = null)
{
...
}

Single Source Of Truth: Each component might have its own state. View and Presenter/ViewModel are meant as the component here. Maintaining states separately at different levels might cause state conflicts. So to avoid these kinds of issues, the state is created at one level (Presenter/ViewModel) and passed to another level (View) in MVI. The only way to change the state is by firing an Interaction by the user. This kind of approach restricts any change to the State of the system only via a defined set of actions. An undefined action by a user cannot cause any undesired change to our System.

NoCallbacks: Usually views are updated via callbacks after the asynchronous call. The view might be detached when the asynchronous call brings the result. So to avoid the crash or improper behavior of the application, we would check whether the instance of the view is available and it is attached. This will pave way for some boilerplate code like below.

These checks and callbacks are handled automatically in MVI using reactive programming that supports observer paradigm.

Hunt the crash: You might be faced situations when it is very difficult to trace and reproduce the crash even though if you have the crash report. The crash report might have the trace of the code flow but does not contain the trace of the view’s state flow before crashing. In MVI tracing the crash becomes easy with the State(Model). In the below code snippet if you see inside the doOnNext closure, logs are registered with the respective action and with the State. Any developer can reproduce the crash with state trace and fix it easily.

Brings back the state: A good app should work without any glitch even in unpredictable circumstances. Suppose orientation change or the activity process gets killed during a phone call, the android activity gets recreated. Restoring the state back was a big task at such situations. MVI library from Mosby maintains the current state and restore the state when the view gets recreated.

Independent UI Components: Every architectural pattern preach to us how the components should be built with no dependencies. The View/Presenter should not be coupled with one another. The responsibility of the View and Presenter is just to render the content and map the data to view respectively.

Multi-platform standard: Reactive programming is a multi-platform standard, so whether it’s a Web or Android or IOS, you might end up joining a multi-platform discussion on Rx. Reactive Codes written in Android(Kotlin) can be shared with IOS(Swift) and vice-versa which makes the code review task easy.

How does the MVI work?

User does an action which will be an Intent → Intent is a state which is an input to model → Model stores state and send the requested state to the View → View Loads the state from Model → Displays to the user. If we observe, the data will always flow from the user and end with the user through intent. It cannot be the other way, Hence its called Unidirectional architecture. If the user does one more action the same cycle is repeated, hence it is Cyclic.

Advantages and Disadvantages of MVI

Let’s see what are the advantages and disadvantages of MVI

Advantages of MVI

  • Maintaining state is no more a challenge with this architecture, As it focuses mainly on states.
  • As it is unidirectional, Data flow can be tracked and predicted easily.
  • It ensures thread safety as the state objects are immutable.
  • Easy to debug, As we know the state of the object when the error occurred.
  • It’s more decoupled as each component fulfills its own responsibility.
  • Testing the app also will be easier as we can map the business logic for each state.

Disadvantages of MVI

  • It leads to lots of boilerplate code as we have to maintain a state for each user action.
  • As we know it has to create lots of objects for all the states. This makes it too costly for app memory management.
  • Handling alert states might be challenging while we handle configuration changes. For example, if there is no internet we will show the snackbar, On configuration change, it shows the snackbar again as its the state of the intent. In terms of usability, this has to be handled.

Creating a Project with MVI architecture

Let’s start by setting up the Android project.

We are trying to achieve this in our Android project

Create a Project

  • Start a new Android project
  • Select Empty Activity and Next
  • Name: MVI-Architecture-Android-Beginners
  • Package name: com.mindorks.framework.mvi
  • Language: Kotlin
  • Finish
  • Your starting project is ready now

Add dependencies

// Added Dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-moshi:2.6.2"
//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"

Project Structure

For the project, We are going to follow a beginner version of MVI. Our package in the project will look like below.

Setup Data Layer

Now, in this section, we will set up the data layer.
Under the data package, we will have api, model, and repository packages. We will create these packages and let’s concentrate on adding classes to each package one by one.

Let’s add classes supporting API.

We need a model to which the response will be transformed. Create User.kt data class as shown below.

import com.squareup.moshi.Json data class User( @Json(name = “id”) val id: Int = 0, 
@Json(name = “name”) val name: String = “”,
@Json(name = “email”) val email: String = “”,
@Json(name = “avatar”) val avatar: String = “” )

Create ApiHelper.kt interface

interface ApiHelper {      
suspend fun getUsers(): List<User>
}

Note: We have used suspend keyword to support Coroutines so that we can call it from a Coroutine or another suspend function.

Kotlin-Coroutines and Flow API are used in this project. You can learn from the following:

Create a class ApiService.kt where we will specify HTTP methods to communicate to the API.

import retrofit2.http.GET  
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}

Now add retrofit builder which will build endpoint URL and consume REST services.

import retrofit2.Retrofit 
import retrofit2.converter.moshi.MoshiConverterFactory
object RetrofitBuilder
{
private const val BASE_URL = "https://test.mockapi.io/"
private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
val apiService: ApiService =
getRetrofit().create(ApiService::class.java)
}

Now we need to implement this interface to fetch the List<Users>, create ApiHelperImpl.kt

class ApiHelperImpl(private val apiService: ApiService) : ApiHelper {      
override suspend fun getUsers(): List<User> {
return apiService.getUsers()
}
}

Now we are ready to communicate with restful services in our data layer.

We will need a repository to requests data. In our case, we are calling the getUsers method from activity through ViewModel to get the user list. Create MainRepository.kt

class MainRepository(private val apiHelper: ApiHelper) 
{
suspend fun getUsers() = apiHelper.getUsers()
}

So our data layer is ready. Coming to UI part now, we need an adapter to recyclerview, Intent for storing user actions, our main activity under view, MainViewModel under viewModel, View state where we have defined different states for which we need to load data to views.

Create MainAdapter in adapter package

import android.view.LayoutInflater 
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.item_layout.view.*
class MainAdapter(private val users: ArrayList<User> ) : RecyclerView.Adapter<MainAdapter.DataViewHolder>()
{
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
{
fun bind(user: User)
{
itemView.textViewUserName.text = user.name
itemView.textViewUserEmail.text = user.email
Glide.with(itemView.imageViewAvatar.context)
.load(user.avatar)
.into(itemView.imageViewAvatar)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
= DataViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_layout, parent,false))

override fun getItemCount(): Int = users.size
override fun onBindViewHolder(holder: DataViewHolder,
position: Int) =
holder.bind(users[position])
fun addData(list: List<User>)
{
users.addAll(list)
}
}

Create MainIntent.kt under the intent package

sealed class MainIntent 
{
object FetchUser : MainIntent()
}

Now adding the MainState.kt under viewstate package. This is the most important part of MVI. In this class, we are defining the states Idle, loading, users, error. Each state can be loaded on to the view by the user intents.

sealed class MainState {      
object Idle : MainState()
object Loading : MainState()
data class Users(val user: List<User>) : MainState()
data class Error(val error: String?) : MainState()
}

Create a ViewModel class — MainViewModel

import androidx.lifecycle.ViewModel 
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
class MainViewModel(private val repository: MainRepository ) :
ViewModel()
{
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState> get() = _state
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch {
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}
private fun fetchUser()
{
viewModelScope.launch
{
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}

Here in the ViewModel, we are observing on the userIntent to perform the action on it.

And based on the response from the data layer, we change the state inside the fetchUser method. And that state is being observed in the MainActivity.

Let us set up ViewModelFactory under the util package.

We are instantiating our viewModel in this class and we will return the instance of the ViewModel.

import androidx.lifecycle.ViewModel 
import androidx.lifecycle.ViewModelProvider
class ViewModelFactory(private val apiHelper: ApiHelper) : ViewModelProvider.Factory
{
override fun <T : ViewModel?> create(modelClass: Class<T>): T
{
if (modelClass.isAssignableFrom(MainViewModel::class.java))
{
return MainViewModel(MainRepository(apiHelper)) as T
}
throw IllegalArgumentException("Unknown class name")
}
}

Now, let’s set up the XML layout.

In the layout folder, update the activity_main.xml with the following code:

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.view.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/buttonFetchUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fetch_user"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

Add item_layout.xml in the layout folder and add the following code:

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/imageViewAvatar"
android:layout_width="60dp"
android:layout_height="0dp"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserName"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="MindOrks" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textViewUserName"
app:layout_constraintTop_toBottomOf="@+id/textViewUserName"
tools:text="MindOrks" /> </androidx.constraintlayout.widget.ConstraintLayout>

Add the following in strings.xml.

<string name="fetch_user">Fetch User</string>

Coming to our MainAcitvity.kt class, We will be adding this under view package. This is the user-facing activity that takes input from the user, based on this MVI checks for the states mentioned in viewModel and loads the particular state in the view.

Let’s see how MainActivity takes care of requesting data, handling states

import android.os.Bundle 
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.mindorks.framework.mvi.ui.main.viewstate.MainState
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity()
{
private lateinit var mainViewModel: MainViewModel
private var adapter = MainAdapter(arrayListOf())
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
setupViewModel()
observeViewModel()
setupClicks()
}
private fun setupClicks()
{
buttonFetchUser.setOnClickListener
{
lifecycleScope.launch
{
mainViewModel.userIntent.send(MainIntent.FetchUser)
}

}
}
private fun setupUI()
{
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.run {
addItemDecoration(DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as
LinearLayoutManager).orientation))
}
recyclerView.adapter = adapter
}
private fun setupViewModel()
{
mainViewModel = ViewModelProviders.of(this, ViewModelFactory(ApiHelperImpl(RetrofitBuilder.apiService ))).get(MainViewModel::class.java)
}
private fun observeViewModel()
{
lifecycleScope.launch
{
mainViewModel.state.collect
{
when (it) {
is MainState.Idle -> {
}
is MainState.Loading -> {
buttonFetchUser.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
is MainState.Users ->
{
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.GONE
renderList(it.user)
}
is MainState.Error ->
{
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.VISIBLE
Toast.makeText(this@MainActivity, it.error,
Toast.LENGTH_LONG).show()
}
}
}
}
}
private fun renderList(users: List<User>)
{
recyclerView.visibility = View.VISIBLE
users.let { listOfUsers -> listOfUsers.let
{ adapter.addData(it)
}
}
adapter.notifyDataSetChanged()
}
}

Here, we are sending the intent to fetch the data on button click(User Action).

Also, we are observing on the ViewModel State for the state changes. And, using “when” condition we are comparing the response intent state and loading the respective states.

Finally, add the Internet Permission in your project. Add the following in the AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET"/>

Now, build the project and run the app on the device. It should load the data into the UI.

Thanks for reading…

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Senior Software Engineer | Android | Java | Kotlin | Xamarin Native Android | Flutter | Go