Principles and processes of MVI architecture
MVI architecture is an architectural pattern based on reactive programming, which divides applications into four core components: Model, View, Intent, and State. principle:
- Model: Responsible for processing the status and logic of data.
- View: Responsible for displaying data and user interface.
- Intent: represents the user's operation, such as button click, input, etc.
- State: reflects the current state of the application.
process:
- The user initiates an intent through the view.
- The Intent is passed to the Model.
- The model updates the state according to the intent.
- Changes in state are passed to the view, and the view updates the interface accordingly.
advantage:
- One-way data flow: One-way data flow ensures state consistency and predictability.
- Responsive features: MVI uses the idea of responsive programming to achieve efficient processing of state changes.
- Ease of testing: Testing the behavior of the model becomes easier due to the clarity of the data flow.
shortcoming:
- Steep learning curve: Compared with traditional MVC or MVP, MVI architecture requires developers to be familiar with the concepts and tools of reactive programming.
- Added some complexity: The introduction of state management and data flow management may increase some complexity.
One-way data flow
User operations notify the Model in the form of Intent => Model updates the State based on the Intent => View receives the State change and refreshes the UI. Data always flows in one direction in a ring structure and cannot flow in the reverse direction:
A Sample to quickly build an MVI architecture project
code example
The code structure is as follows:
Dependent libraries 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.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"
Use the following API in the code to make requests
https://reqres.in/api/users
Will get the result:
1. Data layer
1.1 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 = ""
)
1.2 ApiService
Define ApiService, getUsers method for data request
package com.my.mvi.data.api
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
1.3 Retrofit
Create Retrofit instance
object RetrofitBuilder {
private const val BASE_URL = "https://reqres.in/api/user/1"
private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
1.4 Repository
Define Repository to encapsulate the specific implementation of API requests
package com.my.mvi.data.repository
class MainRepository(private val apiService: ApiService) {
suspend fun getUsers() = apiService.getUsers()
}
2. UI layer
After the Model is defined, start defining the UI layer, including the definition of View, ViewModel and Intent.
2.1 RecyclerView.Adapter
First, a RecyclerView is needed to present the list results. Define 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(): Int = users.size
override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
holder.bind(users[position])
fun addData(list: List<User>) {
users.addAll(list)
}
}
item_layout.xml
<?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"/>
<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>
2.2 Intent
Define Intent to wrap user Action
package com.my.mvi.ui.main.intent
sealed class MainIntent {
object FetchUser : MainIntent()
}
2.3 State
Define the State structure of 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()
}
2.4 ViewModel
ViewModel is the core of MVI, storing and managing State, accepting Intent 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() {
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)
}
}
}
}
We subscribe to userIntent in handleIntent and perform corresponding operations based on the Action type. In this case, when the FetchUser Action appears, the fetchUser method is called to request user data. After the user data is returned, the State will be updated, and MainActivity subscribes to this State and refreshes the interface.
2.5 ViewModelFactory
Constructing ViewModel requires Repository, so inject necessary dependencies through ViewModelFactory
class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(MainRepository(apiService)) as T
}
throw IllegalArgumentException("Unknown class name")
}
}
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() {
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()
}
}
Subscribe to mainViewModel.state in MainActivity and handle various UI display and refresh according to State.
activity_main.xml:
<?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>
As above, a complete MVI project is completed.
Practical explanations and code examples
In order to better understand the MVI architecture, let us demonstrate it through an example. We will create a weather forecast application that displays current weather and weather forecast information for the next few days. In the code examples, we will use the following libraries:
- RxJava: for processing reactive data streams.
- LiveData: used to connect data streams to views.
首先,我们定义模型(Model)的状态(State)类,包含天气预报的相关信息,例如温度、湿度和天气状况等。
data class WeatherState(
val temperature: Float,
val humidity: Float,
val condition: String
)
Next, we create a View interface to display weather information and provide a button to refresh the data.
class WeatherActivity : AppCompatActivity() {
// 初始化ViewModel
private val viewModel: WeatherViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
// 监听状态变化,更新UI
viewModel.weatherState.observe(this, Observer { state ->
// 更新温度、湿度和天气状况的显示
temperatureTextView.text = state.temperature.toString()
humidityTextView.text = state.humidity.toString()
conditionTextView.text = state.condition
})
// 刷新按钮点击事件
refreshButton.setOnClickListener {
// 发送刷新数据的意图
viewModel.processIntent(RefreshIntent)
}
}
}
Then, we create an Intent class that represents the action performed by the user. In this example, we only have an intent to refresh the data.
object RefreshIntent : WeatherIntent
Next, we implement the model part, including state management and data flow processing.
class WeatherViewModel : ViewModel() {
// 状态管理
private val _weatherState = MutableLiveData<WeatherState>()
val weatherState: LiveData<WeatherState> = _weatherState
// 处理意图
fun processIntent(intent: WeatherIntent) {
when (intent) {
RefreshIntent -> fetchWeatherData()
}
}
// 获取天气数据
private fun fetchWeatherData() {
// 发起网络请求或其他数据获取逻辑
// 更新状态
val weatherData = // 获取的天气数据
val newState = WeatherState(
temperature = weatherData.temperature,
humidity = weatherData.humidity,
condition = weatherData.condition
)
_weatherState.value = newState
}
}
The full text explains the architecture of MVI in Android, including principles, project demonstrations and practical exercises. For more advanced learning of Android architecture, please refer to the "Android Core Technology Manual" document. Click to view the detailed content section.
Summarize
The MVI architecture provides a maintainable, testable and responsive architecture pattern through the characteristics of responsive data flow and unidirectional data flow. Although the learning curve is steep, the MVI architecture can better manage status and respond to user operations in the development of large and complex applications. By properly designing the state model and paying attention to side effect management, we can give full play to the advantages of the MVI architecture and improve the maintainability and user experience of the application.