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.