Jetpack Compose 成為主流後,把 Android 開發者往狀態驅動 UI 和單向數據流(UDF)的方向又推了一把。這下子,MVVM 和 MVI 哪個更好的爭論又火起來了。有人説 MVI 有 MVVM 沒有的“可預測性”,也有人覺得 MVVM 不用寫那麼多模板代碼,還能做到 MVI 能做的所有事。本文結合實際的好壞案例、歷史背景,來聊聊為什麼 MVVM 其實大多時候就夠用了。
一點歷史淵源
- 最初的 MVI(2014 年):André Staltz 在他的“響應式 MVC”博客裏介紹了 MVI。他説這是一種響應式 UI 架構模式,通過單向數據流來管理應用裏的狀態和複雜度。
- 最初的 MVVM(2005 年):John Gossman 在微軟為 WPF/Silverlight 搞出來的 MVVM。它強調多個可觀察對象,每個 UI 屬性都綁定到自己的可觀察數據源上。
- 早期的 Android MVVM:模仿了上面的模式:每個 UI 字段(比如 isLoading、title、errorMessage)都用 ObservableField,後來又用 LiveData。
- 現代 MVVM(Compose 時代):谷歌現在推薦的最佳實踐是:用一個不可變的 UI 狀態,通過 StateFlow 來管理,遵循 UDF 原則(“狀態向下,事件向上”)。
實踐對比
反面案例:“上帝式”的 Reducer
好多開發者把 MVI 和一個巨大的 onEvent() 或者 reducer 函數劃等號:
fun reducer(state: UiState, event: UiEvent): UiState {
return when (event) {
is UiEvent.Load -> state.copy(isLoading = true)
is UiEvent.Login -> state.copy(userLoggedIn = true)
is UiEvent.Navigate -> state.copy(navigationTarget = event.destination)
is UiEvent.ShowDialog -> state.copy(isDialogOpen = true)
// 還有 20 多個分支...
}
}
想象一下這個函數就像一個超級大管家,要管的事情太多了。
- 複雜度一上來,所謂的 “可預測性” 就沒了。就好比一個人要同時管幾十件事,很容易亂,你也不知道他下一步會怎麼處理。
- Reducer 變成了 “上帝對象”,把導航、UI 和領域邏輯都混在一起了。本來導航是一個部分的事,UI 顯示是另一個部分的事,現在全塞一塊,就像把不同部門的工作都丟給一個人,又亂又難維護。
- 每加一個新的屏幕功能,模板代碼就暴增。因為每次加功能都要往這個大函數里加分支,就像往一個已經很滿的抽屜裏硬塞東西,越來越亂。
這其實是對 MVI 的誤解,把它當成了一個巨大的 onEvent() 處理器。
正面案例:作用域化 Reducers
data class ItemListState(
val isLoading: Boolean = false,
val items: List<Item> = emptyList(),
val error: String? = null,
)
sealed class ItemListEvent {
object Load : ItemListEvent()
data class Loaded(val items: List<Item>) : ItemListEvent()
data class Failed(val error: String) : ItemListEvent()
}
fun itemListReducer(state: ItemListState, event: ItemListEvent): ItemListState {
return when (event) {
is ItemListEvent.Load -> state.copy(isLoading = true)
is ItemListEvent.Loaded -> state.copy(isLoading = false, items = event.items)
is ItemListEvent.Failed -> state.copy(isLoading = false, error = event.error)
}
}
- Reducer 簡潔,專注於特定功能。就像這個 Reducer 只負責處理 “物品列表” 相關的狀態,不管別的,這樣就很清晰。
- 狀態轉換是可預測的。因為每個事件對應的狀態變化是明確的,比如 Load 事件來了,就把 isLoading 設為 true,很容易知道會發生什麼。
- 副作用(比如網絡請求這些)在其他地方處理(中間件或者用例層)。這就好比,數據加載這種可能耗時的操作,不讓 Reducer 管,而是交給專門的 “助手”(中間件或用例層)去做,Reducer 只負責狀態的轉換,分工更明確。
不過要注意:這裏的做法其實不一定要用 MVI 才能實現。
反面案例:簡單頁面上過度使用 MVI
data class CounterState(val count: Int = 0)
sealed class CounterEvent {
object Increment : CounterEvent()
object Decrement : CounterEvent()
}
fun counterReducer(state: CounterState, event: CounterEvent): CounterState {
return when (event) {
CounterEvent.Increment -> state.copy(count = state.count + 1)
CounterEvent.Decrement -> state.copy(count = state.count - 1)
}
}
對於一個簡單的計數器屏幕,這就太小題大做了。用 MVVM 兩行代碼就能搞定:
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increment() { _count.value++ }
fun decrement() { _count.value-- }
}
正面案例:使用 單一狀態 + Action 的 MVVM
data class UiState(
val isLoading: Boolean = false,
val items: List<Item> = emptyList(),
val error: String? = null
)
class ItemsViewModel(
private val useCase: UseCase
) : ViewModel() {
private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state
fun loadItems() {
_state.update { it.copy(isLoading = true, error = null) }
viewModelScope.launch {
try {
val result = useCase.execute()
_state.update {
it.copy(
isLoading = false,
items = result,
error = null
)
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.message
)
}
}
}
}
fun onItemClicked(item: Item) {
// 更新選中的項目或者觸發導航事件
}
}
- 單一的不可變狀態。整個 UI 的狀態都在 UiState 裏,而且是不可變的,每次更新都是複製一份再改,這樣數據更安全,不容易亂。
- 動作映射到 ViewModel 的函數(而不是用 Reducer)。用户的操作,比如加載數據,就調用 loadItems 函數,很直觀,不像 MVI 還要先搞個事件再給 Reducer 處理。
- 沒有模板代碼,也不需要密封類事件。寫起來更簡潔,不用為了符合 MVI 的結構寫很多額外的類和代碼。
這其實就是 MVVM 裏的 UDF —— 和 MVI 提倡的原則一樣,但更簡單。
觀點討論
支持 MVI 的觀點
- 結構化的 Reducer 和顯式的事件,帶來了可預測性。因為每個事件和狀態轉換都有明確的定義,就像走固定的流程,知道每一步會發生什麼。
- 對於大型團隊和非常複雜、響應式的屏幕,表現更好。大團隊協作時,明確的結構能減少溝通成本,複雜屏幕的狀態多,結構化的管理更不容易出錯。
支持 MVVM 的觀點
- 不用額外的模板代碼,也能實現 Single State + Action + UDF。寫起來更高效,不用為了 MVI 的結構寫很多重複的代碼。
- 已經是主流了,谷歌的指導方針也朝着這個模式走。學起來有更多資料和支持,遇到問題也更容易解決。
- 如果 ViewModel 作用域劃分得好(基於功能,不是 “上帝對象”),完全夠用。就像把 ViewModel 按功能分成不同的小模塊,每個模塊管自己的事,一樣能很好地管理複雜狀態。
我的最終想法
- 簡單屏幕上用 MVI,純屬小題大做。就像做簡單的作業,用複雜的方法反而麻煩。
- 複雜屏幕上,如果合理劃分作用域,MVI 能幫上忙,但用 MVVM + UDF 也能達到同樣的效果。就好比解決複雜問題,MVI 有它的工具,MVVM 也有自己的辦法,不一定非得用 MVI。
- 可預測性不是 MVI 獨有的,清晰的狀態管理就能帶來可預測性,而 MVVM 絕對能做到這一點。只要把狀態管理好,MVVM 一樣能讓狀態變化清晰可預測。
- 很多人誤解了 MVI,把它當成一個巨大的 onEvent () 函數,這並不比臃腫的 ViewModel 好到哪裏去。錯誤使用 MVI,結果和寫得不好的 MVVM 一樣亂。
- 有了 Compose 和 StateFlow,MVVM 完全能解決 MVI 宣稱能解決的所有問題。兩者的區別往往只是語義和模板代碼的不同。所以對於大多數情況,MVVM 已經足夠好用了。