
前言
前段時間寫了一些介紹MVI架構的文章,不過軟體開發上沒有最好的架構,只有最合適的架構,同時眾所周知,Google推薦的是MVVM架構,相信很多人都會有疑問,我為什么不使用官方推薦的MVVM,而要用你說的這個什么MVI架構呢?
不過我這幾天查看Android的應用架構指南,發現谷歌推薦的最佳實踐已經變成了單向資料流動 + 狀態集中管理,這不就是MVI架構嗎? 看起來Google已經開始推薦使用MVI架構了,大家也有必要開始了解一下Android應用架構指南的最新版本了~
本文主要基于Android應用架構指南,感興趣的也可以查看原文
總體架構
兩個架構原則
Android的架構設計原則主要有兩個
分離關注點
要遵循的最重要的原則是分離關注點,一種常見的錯誤是在一個 Activity 或 Fragment 中撰寫所有代碼,這些基于界面的類應僅包含處理界面和作業系統互動的邏輯, 總得來說,Activity或Fragment中的代碼應該盡量精簡,盡量將業務邏輯遷移到其它層
通過資料驅動界面
另一個重要原則是您應該通過資料驅動界面(最好是持久性模型),資料模型獨立于應用中的界面元素和其他組件,
這意味著它們與界面和應用組件的生命周期沒有關聯,但仍會在作業系統決定從記憶體中移除應用的行程時被銷毀,
資料模型與界面元素,生命周期解耦,因此方便復用,同時便于測驗,更加穩定可靠,
推薦的應用架構
基于上一部分提到的常見架構原則,每個應用應至少有兩個層:
- 界面層 - 在螢屏上顯示應用資料,
- 資料層 - 提供所需要的應用資料,
您可以額外添加一個名為“網域層”的架構層,以簡化和復用使用界面層與資料層之間的互動

如上所示,各層之間的依賴關系是單向依賴的,網域層,資料層不依賴于界面層
界面層
界面的作用是在螢屏上顯示應用資料,并回應用戶的點擊,每當資料發生變化時,無論是因為用戶互動(例如按了某個按鈕),還是因為外部輸入(例如網路回應),界面都應隨之更新,以反映這些變化,
不過,從資料層獲取的應用資料的格式通常不同于UI需要展示的資料的格式,因此我們需要將資料層資料轉化為頁面的狀態
因此界面層一般分為兩部分,即UI層與State Holder,State Holder的角色一般由ViewModel承擔

資料層的作用是存盤和管理應用資料,以及提供對應用資料的訪問權限,因此界面層必須執行以下步驟:
- 獲取應用資料,并將其轉換為
UI可以輕松呈現的UI State, - 訂閱
UI State,當頁面狀態發生改變時重繪UI - 接收用戶的輸入事件,并根據相應的事件進行處理,從而重繪
UI State - 根據需要重復第 1-3 步,
主要是一個單向資料流動,如下圖所示:

因此界面層主要需要做以下作業:
- 如何定義
UI State, - 如何使用單向資料流 (
UDF),作為提供和管理UI State的方式, - 如何暴露與更新
UI State - 如何訂閱
UI State
如何定義UI State
如果我們要實作一個新聞串列界面,我們該怎么定義UI State呢?我們將界面需要的所有狀態都封裝在一個data class中,
與之前的MVVM模式的主要區別之一也在這里,即之前通常是一個State對應一個LiveData,而MVI架構則強調對UI State的集中管理
data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf(),
val userMessages: List<Message> = listOf()
)
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)
以上示例中的UI State定義是不可變的,這樣的主要好處是,不可變物件可保證即時提供應用的狀態,這樣一來,UI便可專注于發揮單一作用:讀取UI State并相應地更新其UI元素,因此,切勿直接在UI中修改UI State,違反這個原則會導致同一條資訊有多個可信來源,從而導致資料不一致的問題,
例如,如上中來自UI State的NewsItemUiState物件中的bookmarked標記在Activity類中已更新,那么該標記會與資料層展開競爭,從而產生多資料源的問題,
UI State集中管理的優缺點
在MVVM中我們通常是多個資料流,即一個State對應一個LiveData,而MVI中則是單個資料流,兩者各有什么優缺點?
單個資料流的優點主要在于方便,減少模板代碼,添加一個狀態只需要給data class添加一個屬性即可,可以有效地降低ViewModel與View的通信成本
同時UI State集中管理可以輕松地實作類似MediatorLiveData的效果,比如可能只有在用戶已登錄并且是付費新聞服務訂閱者時,您才需要顯示書簽按鈕,您可以按如下方式定義UI State:
data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf()
){
val canBookmarkNews: Boolean get() = isSignedIn && isPremium
}
如上所示,書簽的可見性是其它兩個屬性的派生屬性,其它兩個屬性發生變化時,canBookmarkNews也會自動變化,當我們需要實作書簽的可見與隱藏邏輯,只需要訂閱canBookmarkNews即可,這樣可以輕松實作類似MediatorLiveData的效果,但是遠比MediatorLiveData要簡單
當然,UI State集中管理也會有一些問題:
- 不相關的資料型別:
UI所需的某些狀態可能是完全相互獨立的,在此類情況下,將這些不同的狀態捆綁在一起的代價可能會超過其優勢,尤其是當其中某個狀態的更新頻率高于其他狀態的更新頻率時, UiState diffing:UiState物件中的欄位越多,資料流就越有可能因為其中一個欄位被更新而發出,由于視圖沒有diffing機制來了解連續發出的資料流是否相同,因此每次發出都會導致視圖更新,當然,我們可以對LiveData或Flow使用distinctUntilChanged()等方法來實作區域重繪,從而解決這個問題
使用單向資料流管理UI State
上文提到,為了保證UI中不能修改狀態,UI State中的元素都是不可變的,那么如何更新UI State呢?
我們一般使用ViewModel作為UI State的容器,因此回應用戶輸入更新UI State主要分為以下幾步:
ViewModel會存盤并公開UI State,UI State是經過ViewModel轉換的應用資料,UI層會向ViewModel發送用戶事件通知,ViewModel會處理用戶操作并更新UI State,- 更新后的狀態將反饋給
UI以進行呈現, - 系統會對導致狀態更改的所有事件重復上述操作,
舉個例子,如果用戶需要給新聞串列加個書簽,那么就需要將事件傳遞給ViewModel,然后ViewModel更新UI State(中間可能有資料層的更新),UI層訂閱UI State訂回應重繪,從而完成頁面重繪,如下圖所示:

為什么使用單向資料流動?
單向資料流動可以實作關注點分離原則,它可以將狀態變化來源位置、轉換位置以及最終使用位置進行分離,
這種分離可讓UI只發揮其名稱所表明的作用:通過觀察UI State變化來顯示頁面資訊,并將用戶輸入傳遞給ViewModel以實作狀態重繪,
換句話說,單向資料流動有助于實作以下幾點:
- 資料一致性,界面只有一個可信來源,
- 可測驗性,狀態來源是獨立的,因此可獨立于界面進行測驗,
- 可維護性,狀態的更改遵循明確定義的模式,即狀態更改是用戶事件及其資料拉取來源共同作用的結果,
暴露與更新UI State
定義好UI State并確定如何管理相應狀態后,下一步是將提供的狀態發送給界面,我們可以使用LiveData或者StateFlow將UI State轉化為資料流并暴露給UI層
為了保證不能在UI中修改狀態,我們應該定義一個可變的StateFlow與一個不可變的StateFlow,如下所示:
class NewsViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
...
}
這樣一來,UI層可以訂閱狀態,而ViewModel也可以修改狀態,以需要執行異步操作的情況為例,可以使用viewModelScope啟動協程,并且可以在操作完成時更新狀態,
class NewsViewModel(
private val repository: NewsRepository,
...
) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
private var fetchJob: Job? = null
fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsForCategory(category)
_uiState.update {
it.copy(newsItems = newsItems)
}
} catch (ioe: IOException) {
// Handle the error and notify the notify the UI when appropriate.
_uiState.update {
val messages = getMessagesFromThrowable(ioe)
it.copy(userMessages = messages)
}
}
}
}
}
在上面的示例中,NewsViewModel 類會嘗試進行網路請求,然后更新UI State,然后UI層可以對其做出適當反應
訂閱UI State
訂閱UI State很簡單,只需要在UI層觀察并重繪UI即可
class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
UI State實作區域重繪
因為MVI架構下實作了UI State的集中管理,因此更新一個屬性就會導致UI State的更新,那么在這種情況下怎么實作區域重繪呢?
我們可以利用distinctUntilChanged實作,distinctUntilChanged只有在值發生變化了之后才會回呼重繪,相當于對屬性做了一個防抖,因此我們可以實作區域重繪,使用方式如下所示
class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Bind the visibility of the progressBar to the state
// of isFetchingArticles.
viewModel.uiState
.map { it.isFetchingArticles }
.distinctUntilChanged()
.collect { progressBar.isVisible = it }
}
}
}
}
當然我們也可以對其進行一定的封裝,給Flow或者LiveData添加一個擴展函式,令其支持監聽屬性即可,使用方式如下所示
class MainActivity : AppCompatActivity() {
private fun initViewModel() {
viewModel.viewStates.run {
//監聽newsList
observeState(this@MainActivity, MainViewState::newsList) {
newsRvAdapter.submitList(it)
}
//監聽網路狀態
observeState(this@MainActivity, MainViewState::fetchStatus) {
//..
}
}
}
}
關于MVI架構下支持屬性監聽,更加詳細地內容可見:
網域層
網域層是位于界面層和資料層之間的可選層,

網域層負責封裝復雜的業務邏輯,或者由多個ViewModel重復使用的簡單業務邏輯,此層是可選的,因為并非所有應用都有這類需求,因此,您應僅在需要時使用該層,
網域層具有以下優勢:
- 避免代碼重復,
- 改善使用網域層類的類的可讀性,
- 改善應用的可測驗性,
- 讓您能夠劃分好職責,從而避免出現大型類,
我感覺對于常見的APP,網域層似乎并沒有必要,對于ViewModel重復的邏輯,使用util來說一般就已足夠
或許網域層適用于特別大型的專案吧,各位可根據自己的需求選用
資料層
資料層主要負責獲取與處理資料的邏輯,資料層由多個Repository組成,其中每個Repository可包含零到多個Data Source,您應該為應用處理的每種不同型別的資料創建一個Repository類,例如,您可以為與電影相關的資料創建 MoviesRepository 類,或者為與付款相關的資料創建 PaymentsRepository 類,當然為了方便,針對只有一個資料源的Repository,也可以將資料源的代碼也寫在Repository,后續有多個資料源時再做拆分

資料層跟之前的MVVM架構下的資料層并沒用什么區別
總結
相比老版的架構指南,新版主要是增加了網域層并修改了界面層,其中網域層是可選的,各位各根據自己的專案需求使用,
而界面層則從MVVM架構變成了MVI架構,強調了資料的單向資料流動與狀態的集中管理,相比MVVM架構,MVI架構主要有以下優點
- 強調資料單向流動,很容易對狀態變化進行跟蹤和回溯,在資料一致性,可測驗性,可維護性上都有一定優勢
- 強調對
UI State的集中管理,只需要訂閱一個ViewState便可獲取頁面的所有狀態,相對MVVM減少了不少模板代碼 - 添加狀態只需要添加一個屬性,降低了
ViewModel與View層的通信成本,將業務邏輯集中在ViewModel中,View層只需要訂閱狀態然后重繪即可
當然在軟體開發中沒有最好的架構,只有最合適的架構,各位可根據情況選用適合專案的架構,實際上在我看來Google在指南中推薦使用MVI而不再是MVVM,很可能是為了統一Android與Compose的架構,因為在Compose中并沒有雙向資料系結,只有單向資料流動,因此MVI是最適合Compose的架構,
當然如果你的專案中沒有使用DataBinding,或許也可以開始嘗試一下使用MVI,不使用DataBinding的MVVM架構切換為MVI成本不高,切換起來也比較簡單,在易用性,資料一致性,可測驗性,可維護性等方面都有一定優勢,后續也可以無縫切換到Compose,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/403993.html
標籤:其他
