在移動應用開發的世界里,架構模式的演進從未停歇。從早期的 MVC 到后來的 MVP、MVVM,每一次變革都在嘗試解決前一代架構的痛點。而今天,我們將探討一種全新的架構模式 ——MVI(Model-View-Intent),它借鑒了前端 React 的單向數據流思想,并結合 Android 開發的最佳實踐,為我們帶來了一種更加清晰、可測試、可維護的代碼結構。
一、架構模式的演進歷程
1.1 MVC:原始的分層嘗試
MVC(Model-View-Controller)是最早被廣泛應用的架構模式之一,它將應用分為三個主要部分:
- Model:負責數據和業務邏輯
- View:負責 UI 展示
- Controller:作為 View 和 Model 之間的橋梁,處理用戶輸入和狀態更新
在 Android 中,Activity 通常扮演 Controller 的角色,但隨著 UI 邏輯的復雜化,Activity 變得越來越臃腫,違反了單一職責原則。
1.2 MVP:引入 Presenter 層解耦
為了解決 MVC 的問題,MVP(Model-View-Presenter)應運而生:
- Model:數據和業務邏輯
- View:負責 UI 展示,通常是一個接口
- Presenter:處理 UI 邏輯,與 Model 交互
MVP 通過接口將 View 和 Presenter 解耦,使得單元測試更加容易。但隨著項目規模增大,Presenter 層可能會變得非常龐大。
1.3 MVVM:數據綁定的力量
MVVM(Model-View-ViewModel)在 MVP 的基礎上引入了數據綁定:
- Model:數據和業務邏輯
- View:負責 UI 展示
- ViewModel:暴露數據流,通過數據綁定與 View 交互
MVVM 通過 LiveData、Data Binding 等技術減少了 View 和 ViewModel 之間的耦合,但它仍然存在一些問題,例如雙向數據流可能導致的復雜性。
1.4 MVI:單向數據流的革命
MVI(Model-View-Intent)是架構模式的最新演進,它借鑒了 React 的單向數據流思想:
- Model:不可變的狀態容器
- View:訂閱狀態并渲染 UI
- Intent:用戶操作轉換為 Intent,觸發狀態更新
MVI 的核心思想是單向數據流和狀態不可變性,所有的狀態變化都遵循一個可預測的流程,使得代碼更加易于理解和調試。
二、MVI 架構的核心概念
2.1 單向數據流
MVI 的核心是單向數據流,它遵循以下流程:
- 用戶操作產生 Intent
- Intent 被 ViewModel 處理
- ViewModel 根據 Intent 更新狀態
- View 訂閱狀態變化并更新 UI
這種單向流動使得數據的流向變得清晰可追溯,大大降低了代碼的復雜性。
2.2 不可變狀態
在 MVI 中,狀態是不可變的。每當有新的狀態產生時,不會修改原有狀態,而是創建一個新的狀態對象。這樣可以確保狀態的變化是可追蹤的,并且簡化了狀態管理。
2.3 狀態容器
ViewModel 作為狀態容器,負責管理和更新應用狀態。它接收 Intent 并根據這些 Intent 生成新的狀態,然后將新狀態發送給 View。
三、MVI 架構的實現
3.1 基礎架構實現
首先,我們需要定義一些基礎接口和類來構建 MVI 架構:
kotlin
// BaseIntent.kt
interface BaseIntent {
}// BaseState.kt
interface BaseState {data object Loading : BaseState // 加載中狀態data object Complete: BaseState // 處理完成data class Error(val message: String = "", val updateId: Long = System.currentTimeMillis()): BaseState // 異常
}// BaseViewModel.kt
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launchabstract class BaseViewModel : ViewModel() {/*** 獲取上下文*/protected fun getContext(): Context = BaseApplication.instance.applicationContext// 接收來自視圖層的意圖(Intent)val intentsFlow = MutableSharedFlow<BaseIntent>()// 用于向視圖層發送狀態更新,類型指定為BaseStateval stateFlow = MutableStateFlow<BaseState?>(null)init {consumption()}/*** 分發消息*/private fun consumption() {viewModelScope.launch {intentsFlow.collect { intent ->handleIntents(intent)}}}abstract fun handleIntents(intent: BaseIntent)// 方法用于向視圖層發送新的BaseState類型的狀態protected fun updateState(newState: BaseState) {viewModelScope.launch {delay(100) // 防止消息太快,丟失stateFlow.value = newState}}
}// BaseActivity.kt
import android.util.Log
import android.view.LayoutInflater
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.launch
import kotlin.reflect.KClassabstract class BaseActivity<VM : BaseViewModel, VB : ViewBinding>(override val vbInflater: (LayoutInflater) -> VB
) : AppCompatActivity() {val tag = this::class.simpleNameprivate val _binding by lazy { vbInflater(layoutInflater) }val binding get() = _bindingopen val viewModel: VM by lazy {initializeViewModel()}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(binding.root)setupObservers()setupIntents()bindClickListener()}/*** 初始化ViewModel*/private fun initializeViewModel(): VM {val classifier = this::class.supertypes[0].arguments[0].type?.classifierLog.d(this::class.simpleName, "initializeViewModel()----->${classifier}")val viewModelClass = (classifier as? KClass<VM>)?.java?: throw IllegalArgumentException("Invalid ViewModel class")return ViewModelProvider(this)[viewModelClass]}/*** 分發狀態*/private fun setupObservers() {lifecycleScope.launch {viewModel.stateFlow.collect { state ->Log.d(tag, "setupObservers()---->$state")render(state)}}}// 抽象方法,由子類實現來根據不同的BaseState狀態渲染UIabstract fun render(state: BaseState?)// 輔助方法,用于發送BaseIntent類型的意圖到ViewModelprotected fun sendIntent(intent: BaseIntent) {lifecycleScope.launch {viewModel.intentsFlow.emit(intent)}}// 設置意圖,由子類實現open fun setupIntents() {}// 綁定點擊事件,由子類實現open fun bindClickListener() {}
}
3.2 登錄功能的 MVI 實現
接下來,我們以登錄功能為例,展示如何使用 MVI 架構實現一個完整的功能模塊。
首先是布局文件:
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"><androidx.appcompat.widget.AppCompatEditTextandroid:id="@+id/et_username"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toTopOf="@+id/et_password"app:layout_constraintVertical_chainStyle="packed"android:layout_marginStart="12dp"android:layout_marginEnd="12dp"android:layout_marginBottom="12dp"android:layout_width="match_parent"android:hint="手機號"android:layout_height="wrap_content"/><androidx.appcompat.widget.AppCompatEditTextandroid:id="@+id/et_password"android:layout_width="match_parent"app:layout_constraintTop_toBottomOf="@+id/et_username"app:layout_constraintBottom_toTopOf="@+id/btn_login"android:hint="密碼"android:layout_margin="12dp"app:layout_constraintVertical_chainStyle="packed"android:layout_height="wrap_content"/><androidx.appcompat.widget.AppCompatButtonandroid:id="@+id/btn_login"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="24dp"android:layout_marginStart="12dp"android:layout_marginEnd="12dp"android:textColor="@color/white"android:text="Login"android:background="@drawable/selector_rounded_button"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintTop_toBottomOf="@+id/et_password"/><com.google.android.material.progressindicator.CircularProgressIndicatorandroid:id="@+id/progressBar"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:indeterminateTint="@color/main_color"app:indicatorColor="@color/main_color"app:trackColor="@color/main_color"android:layout_width="40dp"android:visibility="gone"android:layout_height="40dp"/></androidx.constraintlayout.widget.ConstraintLayout>
然后定義登錄相關的 Intent、State 和 ViewModel:
kotlin
// LoginIntent.kt
sealed class LoginIntent: BaseIntent {data class EntryPhone(val phone: String): LoginIntent() // 手機號的意圖data class EntryPassword(val password: String): LoginIntent() // 密碼的意圖data object Login: LoginIntent() // 點擊登錄
}// LoginViewState.kt
sealed class LoginViewState: BaseState {data class LoginStatus(val highlight: Boolean) : LoginViewState()// 高亮狀態data object Loading : LoginViewState() // 加載中狀態data class Success(val user: UserBean?) : LoginViewState() // 成功登錄狀態data class Error(val message: String) : LoginViewState() // 登錄錯誤狀態
}// LoginViewModel.kt
@HiltViewModel
class LoginViewModel @Inject constructor (private val repository: LoginRepository,
) : BaseViewModel() {private var phone: String = ""private var password: String = ""// 設置登錄按鈕是否可用private fun setLoginButtonEnabled(enabled: Boolean) {updateState(LoginViewState.LoginStatus(enabled))}/*** 調用登錄*/private fun login() {updateState(LoginViewState.Loading)viewModelScope.launch {repository.login(phone, password).onSuccess {updateState(LoginViewState.Success(it))}.onFailure {updateState(LoginViewState.Error(it.message.toString()))}}}/*** 處理意圖*/override fun handleIntents(intent: BaseIntent) {when(intent) {is LoginIntent.EntryPhone -> {phone = intent.phonesetLoginButtonEnabled(phone.length >= 6 && password.length >= 6)}is LoginIntent.EntryPassword -> {password = intent.passwordsetLoginButtonEnabled(password.length >= 6 && phone.length >= 6)}is LoginIntent.Login -> {login()}}}
}
最后是 Activity 的實現:
kotlin
// LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : BaseActivity<LoginViewModel, ActivityLoginBinding>(ActivityLoginBinding::inflate
) {/*** 監聽狀態*/override fun render(state: BaseState?) {when(state) {is LoginViewState.LoginStatus -> {setLoginButtonEnabled(state.highlight)}is LoginViewState.Loading -> {showLoading()}is LoginViewState.Success -> {if (state.user != null) {showSuccess(state.user)}}is LoginViewState.Error -> {showError(state.message)}}}/*** View視圖與Intent意圖進行綁定*/override fun setupIntents() {binding.etUsername.addTextChangedListener { text ->sendIntent(LoginIntent.EntryPhone(text.toString()))}binding.etPassword.addTextChangedListener { text ->sendIntent(LoginIntent.EntryPassword(text.toString()))}binding.etUsername.setText("1234567890")binding.etPassword.setText("54321#")}/*** 設置點擊事件*/override fun bindClickListener() {super.bindClickListener()binding.btnLogin.setOnClickListener {sendIntent(LoginIntent.Login)}}/*** 登錄成功*/private fun showSuccess(user: UserBean) {binding.progressBar.visibility = View.GONEToast.makeText(this, "Login successful: ${user.userName}", Toast.LENGTH_SHORT).show()}/*** 顯示錯誤信息*/override fun showError(errorMessage: String?) {super.showError(errorMessage)binding.progressBar.visibility = View.GONEToast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()}// 登錄按鈕是否可用private fun setLoginButtonEnabled(enabled: Boolean) {binding.btnLogin.isEnabled = enabled}
}
四、MVI 架構的優勢與挑戰
4.1 優勢
- 單向數據流:數據流動方向明確,易于理解和調試
- 可測試性:ViewModel 不依賴 View,單元測試更加簡單
- 狀態可追溯:所有狀態變化都可以追蹤,便于定位問題
- 解耦徹底:View 和 Model 完全解耦,職責更加清晰
- 一致性:所有狀態更新都遵循相同的模式,代碼風格統一
4.2 挑戰
- 學習曲線:相較于 MVVM,MVI 的概念更加抽象,需要一定的時間來理解
- 初期開發效率:需要編寫更多的代碼(Intent 和 State),初期開發效率可能較低
- 狀態管理復雜度:在復雜場景下,狀態管理可能變得復雜,需要合理設計狀態結構
五、總結
MVI 架構通過引入單向數據流和不可變狀態,為 Android 開發帶來了一種更加清晰、可測試、可維護的代碼結構。雖然初期學習和開發成本較高,但在大型項目中,MVI 的優勢將得到充分體現。
如果你正在尋找一種能夠徹底解決 View 和 Model 耦合問題的架構模式,那么 MVI 絕對值得一試。通過遵循 MVI 的設計原則,你可以構建出更加健壯、易于維護的 Android 應用。
希望本文能夠幫助你理解 MVI 架構的核心概念和實現方式,讓你在 Android 開發的道路上更進一步。