MIV architecture

Getting to know the MVI architecture pattern in Android

MVI architecture proposed and implemented a new theory that by writing a method only once, it is possible to control all UI interactions with the user and actually get rid of CallBacks to some extent.

In the following, we examine the MVI architecture, compare it with other architectures such as the MVP architecture , and finally do a project with it. If you haven’t heard the name of this architecture until now, or you want to use it, stay with Son Learn’s programming training website until the end of this article.

What is MVI architecture?

MVI architecture is an emerging architecture that has been published in the last few years. This architecture is based on Model-View-Intent and is built on two principles (one-way) and (cyclic) inspired by the Cycle.js framework. MVI architecture has many differences compared to other architectures such as MVP, MVC or MVVM, which we will discuss further.

 

What do the three words Model-View-Intent mean?

As we mentioned in the previous part, MVI architecture is based on Model, View and Intent. In the following, we will define these three words:

Model: provides different states in the application. Models in the MVI architecture should be immutable, and data flows between it and other application layers should be unidirectional.

View : Like other architectures, it uses interfaces and creates contracts for Views such as Activities and Fragments and implements them.

Intent : declare any activity. This activity can be created by the user or the application itself, for all activities (actions), the View receives an Intent, and the Presenter listens to the Intent (Observes).

What are the advantages and disadvantages of MVI architecture?

In the previous parts, we introduced the MVI architecture. Now we want to examine the advantages and disadvantages of MVI architecture in detail.

What are the advantages of MVI architecture?

Advantages of MVI architecture that can be mentioned:

  • Data in MVI architecture is unidirectional and has a cyclical data flow.
  • In general, maintaining State in this architecture is not a challenge because the main focus of this architecture is on this issue.
  • During the life cycle of the View application, it controls only one state and informs different layers.
  • It is easier to debug the application because the states are clear at each stage.
  • Models in the MVI architecture are immutable, which makes it difficult to move between threads or change data in large applications.

Disadvantages of MVI architecture

Disadvantages of MVI architecture that can be mentioned:

  • The main difference that can be listed in the MVI architecture is the difficulty in understanding it for novice and inexperienced programmers. Because to understand this architecture, you need to understand other concepts such as Reactive Programming, Multi-Threading, RxJava, etc.
  • In the MVI architecture, because we create a new object of our model for each state, problems such as memory disruption or memory leak may arise as time passes and the application grows. You must pay attention to this point when developing the software.
  • The MVI architecture causes code repetition in different parts of the application because it creates a new state for each user interaction.

What are the capabilities of MVI architecture?

In the previous part, we explained that Models in the MVI architecture are State maintainers, and this has made this architecture popular. This feature makes the Model control all the data in the application and send it to higher or lower layers of the application if needed.

Another thing that developers deal with in large projects is the security of data in threads, when moving data in different threads, there is a possibility of data problems. MVI architecture has solved this problem to some extent.

In the MVI architecture, not all layers have the possibility to change the data in the Model. For example, views can only capture the state and display it using one method.

 

What are the differences between MVI and MVP architecture?

In general, MVP and MVI architectures are similar in most cases, but the main difference between these two architectures is in two cases, which we will mention below:

Models in MVI architecture

When in reactive programming, there is a reaction at the UI level or a change from the user level or the application itself, for example by clicking a button or sending a request to the server and displaying its result. A new state is created. These changes can happen in the background of the application or at the UI level, such as displaying a ProgressBar or receiving the result from the server side.

To understand better, imagine a Model in MVP architecture. This class holds the result received from the server, which is as follows:

data  class  Picture (
         var  id :  Int ? =  null ,
         var  name :  String ? =  null ,
         var  url :  String ? =  null ,
         var  date :  String ? =  null 
) 

The above example Presenter uses the Model above and prepares it for display at the UI level:

class MainPresenter(private  var  view: MainView?) {
    override fun  onViewCreated () {
        view. showLoading ()
        loadPictureList { pictureList ->
            pictureList.let {
                this. onQuerySuccess (pictureList)
            }
        }
    }
    override fun onQuerySuccess(data: List<Picture>) {
        view. hideLoading ()
        view. displayPictureList ()
    }
}
 

In such a case, there is no particular problem, but the MVI architecture has investigated and solved some of the problems that may arise in the meantime:

  • Receiving multiple inputs and in MVVM and MVP architectures, several inputs and outputs must be controlled inside the Presenter and ViewModel, which can be problematic as time passes and the application grows.
  • The existence of several different states In MVVM : and MVP architectures, there may be different states for the application that the developer controls and programs with CallBacks. This may be problematic in exceptional circumstances.

In such cases, the MVI architecture helps us to solve these problems, in which case our model is as follows:

sealed class PictureState {
     object  LoadingState :  PictureState ()
    data class  DataState (val data: List<Picture>) :  PictureState ()
    data class  ErrorState (val data: String) :  PictureState ()
    data class  ConfirmationState (val picture: Picture) :  PictureState ()
    object FinishState :  PictureState ()
}
 

In fact, Models in MVI architecture try to control different states and announce them to ViewModel, View or Presenter, which prevents repeating the coding of a State in different layers of the application.

And also our Presenter in this example is as follows:

class  MainPresenter  {
    private val compositeDisposable =  CompositeDisposable ()
    private lateinit  var  view :  MainView 
    fun  bind ( view: MainView ) {
         this . view  = view
        compositeDisposable. add ( observePictureDeleteIntent ())
        compositeDisposable. add ( observePictureDisplay ())
    }
    fun  unbind ( ) {
         if  (!compositeDisposable. isDisposed ) {
            compositeDisposable. dispose ()
        }
    }
    private fun  observePictureDisplay () =  loadPictureList ()
            . observeOn ( AndroidSchedulers . mainThread ())
            . doOnSubscribe  { view. render ( PictureState . LoadingState ) }
            . doOnNext  { view. render (it) }
            . subscribe ()
}
 

Now, your application has only one output, which is the State of a View, and it can be displayed using the render() method, this method informs the View of the current state of the application. We make sure that during the lifetime of the application Model only controls one state and cannot be changed in other classes. In other words, it shows only one mode during the life of the application.

 

Views and Intents in MVI architecture

Like the MVP architecture, the MVI architecture creates an interface as a contract for one or more Views and is implemented by one or more Activities or Fragments. Views in the MVI architecture tend to use a render() and use it to display the UI to the user. Also, Views use intents that are Observable to respond to user interaction with the application.

Note: Intents in the Android MVI architecture do not mean the android.content.Intent class. Rather, it means a class that expresses the changes and performance of the application.

In this case, the View is as follows:

class  MainActivity  :  MainView  {
    override fun  onCreate ( savedInstanceState: Bundle? ) {
         super . onCreate (savedInstanceState)
         setContentView (R. layout . activity_main )
    }
     //1 
    override fun  displayPictureIntent () = button. clicks ()
     //2 
    override fun  render ( state: PictureState ) {
        when (state) {
            is  PictureState . DataState  ->  renderDataState (state)
            is  PictureState . LoadingState  ->  renderLoadingState ()
            is  PictureState . ErrorState  ->  renderErrorState (state)
        }
    }
     //4 
    private fun  renderDataState ( dataState: PictureState.DataState ) {
         //Render picture list 
    }
     //3 
    private fun  renderLoadingState ( ) {
         //Render progress bar on screen 
    }
     //5 
    private fun  renderErrorState ( errorState: PictureState.ErrorState ) {
         //Display error mesage 
    }
}
 
  1. method displayMovieIntent : Binds user interactions to the appropriate Intent. In this example, clicking on a button is reported as an intent.
  2. method render : indicates the current state of the View. It is also part of the MainView interface.
  3. method renderDataState : This method prepares the data received from the Model for display.
  4. method renderLoadingState : This method displays the loading state in View.
  5. method renderErrorState : This method displays the error generated in the View.

Creating an Android project with MVI architecture

In the following, we are going to implement an Android project for a better understanding:

Note: This project requires knowledge of concepts such as Coroutine, Retrofit and Glide.

To create a new project, do the following steps in order:

  1. Select the Create a New Project option.
  2. Select Empty Activity and click Next.
  3. Choose the desired name, choose the storage location and choose the Kotlin language for the project and then click on the Finish option.

After completing the above steps, we have created our project and proceed to the next steps:

We use three components in this project: Glide, Retrofit and Coroutine. To use these libraries, add the following dependencies in the build.gradle file (you can see the latest version available on the manufacturer’s site or in the Android documentation):

//lifecycle 
implementation  'androidx.lifecycle:lifecycle-viewmodel-ktx:{last-version}' 
implementation  'androidx.lifecycle:lifecycle-runtime-ktx:{last-version}' 
//glide 
implementation  'com.github.bumptech.glide:glide:{last-version}' 
//retrofit 
implementation  'com.squareup.retrofit2:retrofit:{last-version}' 
implementation  "com.squareup.retrofit2:converter-moshi:{last-version}" 
//coroutine 
implementation  "org.jetbrains.kotlinx:kotlinx-coroutines-android:{last-version}" 
implementation  "org.jetbrains.kotlinx:kotlinx-coroutines-core:{last-version}" 

Next, we do the packaging for our project:

 

Before we start, in the AndroidManifest.xml file we add the following access:

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

In the first step, we need a Model as follows, which we call User (add this class to the repository package):

data  class  User (
        val  id :  Int ? =  null ,
        val  name :  String ? =  null ,
        val  email :  String ? =  null  
)
 

In the next step, we create the following classes in the api package:

In this project, we need a class called ApiService to be able to use the Retrofit library:

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

Next, we create an interface called ApiHelper:

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

Note: In order to be able to use Coroutine, we must change our method (Function) to suspend fun.

Now we create an Object called RetrofitBuilder and put the following methods in it:

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

In the next step, we need a class to implement the ApiHelper interface, which is as follows:

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

Up to this point, the request to our server part as well as the Retrofit implementation is complete. In the next step, we need a repository so that we can use it to call the getUser method in the ViewModel (add this class to the repository package):

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

Our model package is ready in this project. In the next step, we will create a package called adapter in the ui section. To implement RecyclerView, we need a class to implement Adapter and ViewHolder, which we create and write as follows:

import  android. view . LayoutInflater 
import  android. view . View 
import  android. view . ViewGroup 
import  androidx. recyclerview . widget . RecyclerView 
import  com. bumptech . glide . Glide 
import  com. sevenlearn . mvi . R 
import  com. sevenlearn . mvi . data . model . User 
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 
        }
    }
    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)
    }
}
 

Then we create another package called intent and add the following class to it (you can put this class in the package named intent in the UI package):

sealed class MainIntent {
     object  FetchUser :  MainIntent ()
}
 

In the next step, we reach the important part of the application, the MainState class, this class is one of the most important parts of the application in the MVI architecture, which we add in a package named viewstate:

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

Then we create our ViewModel and call it MainViewModel:

import  androidx. lifecycle . ViewModel 
import  androidx. lifecycle . viewModelScope 
import  com. sevenlearn . mvi . data . repository . MainRepository 
import  com. sevenlearn . mvi . ui . main . intent . MainIntent 
import  com. sevenlearn . mvi . ui . main . viewstate . MainState 
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 )
            }
        }
    }
}