Benefit of Adapting the MVI architecture pattern in Android App Project

Refrain from getting confused between MVI and MVC. Both serve different purposes for your **Android app development **projects. Each initiated to resolve another problem through a distinct architectural pattern before moving on to the MVI architecture benefits.

MVC- Model View Controller

You must be aware of this concept of UI application development. It manages three distinct purposes via three components. The idea of deriving the MVC model is derived from collecting the data rendered on screen. Alternatively, this model defines the integration of business logic, data visibility properties, and manipulation control.

Model is the separate component responsible for the business logic's rendering and behavior.

The View is another component of UI responsible for managing the properties of shape, color, and tools that respond over the click events.

Control is responsible for modification of the model after getting the response of View appropriately.

MVI Architecture Pattern- Model View Intent

Do you aware of finite state machine concepts?

The MVI architecture pattern model is based on a similar thing. At each UI upgradation, a new state is initialized. Generally, when we want to keep a record of any activity, we take screenshots so we can go back and review things. These screenshot records help us to rectify and correct the mistakes.

MVI initiates the same approach to manage things in real-world UI development. The model asks us to debug, evaluate, test, and reconstruct the UI state events.

Basically, when we are willing to take the next move, it is based on specific Intent. We know what the issue and things may resolve after the upgradation. Everything is in the process of satisfying the user's expectations. You may be trying to do anything with the screen. The developer knows how the components will perform the responsibility of data rendering and click events behavior.

Here, the Intent means the user's expectation binds with the action. Usually, ViewModel is responsible for displaying the intents or actions previews. In other words, the View model will take care of model behaviors.

What are the impressive benefits of the MVI Architecture Pattern?

  • MVI architecture pattern follows the concept of reactive programming or unidirectional/cyclical dataflow.
  • It simplified the UI development easy to manageable coding and testing practices.
  • Over the years, UI development has witnessed a top-notch evolution. Developers perform numerous upgradation to the client side. The know-how though it is to track the ongoing changes and upgradation. Too many heavy UI state manipulations will influence the application behavior.
  • MVI architectural approach harnesses the hardship of coming across UI development. It properly structures explicit transitions and restricts unrecognized states by keeping everything on track.
  • While working with the MVI, it goes as View> ViewModel > Model > View At each update, you can see the previous issues/ views and the new updated ones. In View, developers get ideas or work on predictable actions or moves to improve things.
  • MVI needs to emphasize the discrete moves and presentation elements more.
  • In this model, immutable state objects are present with thread safety.

Practicing the MVI architecture with Android Studio

Initiate a project based on the **Android Studio **Platform.

  • Go ahead with Empty Activity; proceed with Next.
  • Name the project.
  • Define the package name: com.mindorks.framework.mvi
  • Set the Language Preference to Kotlin.
  • You reach the final stage.
  • Initial requirements are done.

Next, we need to integrate/ include packages or 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"

Now, please proceed to the data layer to access the API, model, and repository packages.

Then, we will initiate the classes for API and the model for any manipulation.

package com.mindorks.framework.mvi.data.model

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

package com.mindorks.framework.mvi.data.api

import com.mindorks.framework.mvi.data.model.User

interface ApiHelper {

    suspend fun getUsers(): List<User>

}

The android app development project will access Kotlin-Coroutines and Kotlin Flow API. Next, we need to manage the things for API communication through the protocol method HTTP.

package com.mindorks.framework.mvi.data.api

import com.mindorks.framework.mvi.data.model.User
import retrofit2.http.GET

interface ApiService {

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

We need to initiate the endpoint URL and other REST services.

package com.mindorks.framework.mvi.data.api

import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

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)
}

Initiate the ApiHelperImpl.kt to fetch the users list.

package com.mindorks.framework.mvi.data.api

import com.mindorks.framework.mvi.data.model.User

class ApiHelperImpl(private val apiService: ApiService) : ApiHelper {

    override suspend fun getUsers(): List<User> {
        return apiService.getUsers()
    }
}

Create MainRepository.kt it manage the data request.

package com.mindorks.framework.mvi.data.repository

import com.mindorks.framework.mvi.data.api.ApiHelper


class MainRepository(private val apiHelper: ApiHelper) {

    suspend fun getUsers() = apiHelper.getUsers()

}

Data layer part is done. Now initiate the MainAdapter for recyclerview, access Intent for useraction, MainViewModel also looks for View state for load data and state.

package com.mindorks.framework.mvi.ui.main.adapter

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

}

Jump intent package and initiate the MainIntent.kt.
package com.mindorks.framework.mvi.ui.main.intent

sealed class MainIntent {

    object FetchUser : MainIntent()

}

Let's move to achieve the real purpose, loading, users, error, states idle, and let the view download.

package com.mindorks.framework.mvi.ui.main.viewstate

import com.mindorks.framework.mvi.data.model.User

sealed class MainState {

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

}

Let’s initiate the class ViewModel

package com.mindorks.framework.mvi.ui.main.viewmodel

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

It is initiated to fulfill the user's purpose or intent. We get the response over the data layer. State manipulation is happening through the fetchuser method. Everything is reviewed through the MainActivity.

Initiate the ViewModelFactory. Here we will get the object's response for the ViewModel.

package com.mindorks.framework.mvi.util

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.mindorks.framework.mvi.data.api.ApiHelper
import com.mindorks.framework.mvi.data.repository.MainRepository
import com.mindorks.framework.mvi.ui.main.viewmodel.MainViewModel

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")
    }

} 

Initiate the XML layout. Modify the activity_main.xml inside the layout folder.

<?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>

Initate the item_layout.
<?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>

Include this also: <string name="fetch_user">Fetch User</string>

Let’s jump over the MainActivity.kt class, and launch under the view package. Here all the mentioned states will get loaded inside the view.

From data requests to states, everything will be smoothly managed under MainActivity.

package com.mindorks.framework.mvi.ui.main.view

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.R
import com.mindorks.framework.mvi.data.api.ApiHelperImpl
import com.mindorks.framework.mvi.data.api.RetrofitBuilder
import com.mindorks.framework.mvi.data.model.User
import com.mindorks.framework.mvi.util.ViewModelFactory
import com.mindorks.framework.mvi.ui.main.adapter.MainAdapter
import com.mindorks.framework.mvi.ui.main.intent.MainIntent
import com.mindorks.framework.mvi.ui.main.viewmodel.MainViewModel
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 will wait for the intended response of data fetching on the click event. Subsequently, we will wait for the state manipulation and initiate the when condition for proper comparison of state loading and intent state response.

The Last thing left to do is integrating the internet permission. Access AndroidManifest.xml:

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

It’s enough for a simple Android App Development project. To access the advanced functionalities, you may use the framework for some instance features, base class, API error management, and interface initialization. Further, integrate the Kotlin extension, and perform unit testing.

At the End:

The MVI architecture pattern is an arrangement that handles the app layer’s state management. It manages things in a clean format with scalable codebase scalability. However, it increases the memory size due to multiple state records, object creation, and boilerplate. Additionally, things become challenging with configuration updates or poor internet connection. For usability purposes, developers must look over the snack bar and state the intent.


Richard Thompson picture

This post is really interesting.