Nowadays, Android development requires some knowledge of architecture, from MVC, MVP to MVVM, and presumably everyone has long been familiar with their advantages and disadvantages. The MVI introduced today is very close to MVVM and can be targeted to remedy some of the shortcomings in MVVM

What is a MVI?

MVInamelyModel-View-Intent, inspired by cyces.js front-end framework, it advocates a design idea of one-way data flow, which is very suitable for data-driven UI display projects:

  • Model: Unlike models in other MVVMS, MVI’s Model mainly refers to UI State. The current interface is nothing more than a snapshot of the UI state: data loading, control location, and so on are UI states
  • View: As with any MVX View, it can be an Activity, Fragment, or any UI host unit. Views in MVI refresh the interface by subscribing to changes in intEnts.
  • Intent: This Intent is not an Intent of an Activity. Any action by the user is wrapped as an Intent and sent to Model for data requests


Unidirectional data flow

User action notifies Model in the form of an Intent to update State => View to receive State changes and refresh the UI.

Data always flows in one direction in a circular structure, not the other:What are the advantages and disadvantages of MVI for this one-way data flow structure?

  • advantages

    • All changes to the UI come from State, so just focusing on State makes the architecture simpler and easier to debug
    • Data flows in one direction, making it easy to track and trace state changes
    • State instances are immutable to ensure thread-safety
    • The UI simply reflects State changes, no additional logic, and can be easily replaced or reused
  • disadvantages

    • All operations will eventually be converted to State, so it’s easy to inflate the State of complex pages
    • State is constant, and every time the state needs to be updated, a new object is created to replace the old object, which incurs some memory overhead
    • Some UI changes to event classes are inappropriate to describe with state, such as popping up a toast or snackbar

Talk is cheap, show me the code. Let’s use a Sample to see how to quickly build an MVI architecture project.

Code sample

The code structure is as follows:

The dependency library in Sample

// 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. Making. 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." "
Copy the code

The following API is used in the code to make the request

https://reqres.in/api/users
Copy the code

The result will be:


1. The data layer

1.1 the User

Define User’s data class

package com.my.mvi.data.model

data class User(
    @Json(name = "id")
    val id: Int = 0.@Json(name = "first_name")
    val name: String = "".@Json(name = "email")
    val email: String = "".@Json(name = "avator")
    val avator: String = ""
)
Copy the code

1.2 ApiService

Define ApiService, getUsers method for data requests

package com.my.mvi.data.api

interface ApiService {

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

Copy the code

1.3 Retrofit

Create a Retrofit instance


object RetrofitBuilder {

    private const val BASE_URL = "https://reqres.in/api/user/1"

    private fun getRetrofit(a) = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()


    val apiService: ApiService = getRetrofit().create(ApiService::class.java)

}
Copy the code

1.4 the Repository

Define a Repository that encapsulates the concrete implementation of API requests

package com.my.mvi.data.repository

class MainRepository(private val apiService: ApiService) {

    suspend fun getUsers(a) = apiService.getUsers()

}
Copy the code

2. The UI layer

Once the Model is defined, start defining the UI layer, including the View, ViewModel, and Intent definitions

2.1 RecyclerView. Adapter

First, we need a RecyclerView to render the list results. We define the MainAdapter as follows:

package com.my.mvi.ui.main.adapter

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(a): Int = users.size

    override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
        holder.bind(users[position])

    fun addData(list: List<User>) {
        users.addAll(list)
    }

}
Copy the code

item_layout.xml


      
<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"/>

    <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" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

2.2 the Intent

Define the Intent to wrap the user Action

package com.my.mvi.ui.main.intent

sealed class MainIntent {

    object FetchUser : MainIntent()

}
Copy the code

2.3 the State

Define the State structure for the UI layer

sealed class MainState {

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

}
Copy the code

2.4 the ViewModel

The ViewModel is the heart of the MVI, storing and managing states, and accepting intEnts and making data requests

package com.my.mvi.ui.main.viewmodel

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(a) {
        viewModelScope.launch {
            userIntent.consumeAsFlow().collect {
                when (it) {
                    is MainIntent.FetchUser -> fetchUser()
                }
            }
        }
    }

    private fun fetchUser(a) {
        viewModelScope.launch {
            _state.value = MainState.Loading
            _state.value = try {
                MainState.Users(repository.getUsers())
            } catch (e: Exception) {
                MainState.Error(e.localizedMessage)
            }
        }
    }
}
Copy the code

We subscribe to the userIntent in the handleIntent and perform the Action based on the Action type. In this case, when the FetchUser Action occurs, the FetchUser method is called to request user data. When the user data is returned, the State is updated, and MainActivity subscribes to the State and refreshes the interface.

2.5 ViewModelFactory

Constructing a ViewModel requires a Repository, so inject the necessary dependencies through the ViewModelFactory

class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {

    override fun 
        create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(MainRepository(apiService)) as T
        }
        throw IllegalArgumentException("Unknown class name")}}Copy the code

2.6 define MainActivity

package com.my.mvi.ui.main.view

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(a) {
        buttonFetchUser.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.userIntent.send(MainIntent.FetchUser)
            }
        }
    }


    private fun setupUI(a) {
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.run {
            addItemDecoration(
                DividerItemDecoration(
                    recyclerView.context,
                    (recyclerView.layoutManager as LinearLayoutManager).orientation
                )
            )
        }
        recyclerView.adapter = adapter
    }


    private fun setupViewModel(a) {
        mainViewModel = ViewModelProviders.of(
            this,
            ViewModelFactory(
                ApiHelperImpl(
                    RetrofitBuilder.apiService
                )
            )
        ).get(MainViewModel::class.java)
    }

    private fun observeViewModel(a) {
        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()
    }
}
Copy the code

Subscribe to mainViewModel.state in MainActivity to handle various UI displays and refreshes based on state.

activity_main.xml:


      
<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>
Copy the code

As above, a complete MVI project is complete.

The last

On the basis of MVVM, MVI provides one-way flow of data and immutability of state, which is similar to the idea of Redux in the front end and is very suitable for the scenario of UI presentation class. Neither MVVM nor MVI is the final form of architecture. There is no perfect architecture in the world. It is necessary to choose the appropriate architecture for development according to the project situation.