一文了解MVI架構,學起來吧~
前言
大約在去年11月份,Google將官方網站上推薦的MVVM架構悄悄替換成了MVI架構。參考了官方與許多前輩的分享,便有了此文。不過下面的再前言應當是每個讀者心中所需要認定的。
再前言
總覽我所有的部落格,我很少寫關於架構模式相關的文章。因為我覺得:
不管是從剛開始所使用的MVP、MVVM再到現在Google官方所推薦的MVI架構,我希望各位讀者千萬不要將教條主義當真理。官方推薦了MVVM就馬上去踩MVP,官方推薦了MVI就馬上去踩MVVM,甚至使用MVVM的開發者會鄙視使用MVP的,使用MVI的開發者會鄙視使用MVVM,這一點真是滑稽。
其實完全沒必要如此,符合專案本身才是最好的架構。許多技術交流群中趣稱:“Google工程師為了KPI 苦了廣大開發者”。這讓我想到“大明風華”中的一句臺詞~
MVI架構
好了,廢話說了這麼多,我們來看MVI架構是什麼樣子的,直接看官方網站的一張圖,如下所示:
MVI中 分為UI層、網域層、與資料層,我造個詞叫他UDD,其中網域層可有可無,網域層我們最後再來看。我這裡不會再一一截圖來展示UI層怎麼樣、資料層怎麼樣,可直接看官網。(其實相比較於MVVM基本沒變化)
MVI中的I是Intent即為使用者意圖,如點選事件、重新整理等都是Intent。那麼MVI到底解決了MVVM中的什麼問題呢?
我現在所給出的答案就是: 集中管理State與使用者意圖管理 (單資料流) ,單資料流就是狀態向下流動、事件向上流動的模式。接下來我們著重來看這兩點。
集中管理State
在MVVM樣式的程式碼中,以網路請求功能為例,UI狀態分為正在載入、載入成功與載入失敗,為了監聽UI狀態,我們會在Viewmodel中定義變數監聽,程式碼如下所示:
```
/**
* 是否正在載入
/
private val isLoding = MutableSharedFlow
/**
* 載入成功
/
private val loadSuccess = MutableSharedFlow
/**
* 載入失敗
/
private val loadFailed = MutableSharedFlow
由於不能破壞資料的封裝性,所以我們要定義一個私有的不可變的MutableSharedFlow用於在Viewmodel賦值,再對外暴露一個不可變的用於在UI層監聽。UI中的監聽程式碼如下所示:
viewModel._isLoding.collect {
if (it){
//顯示彈窗
}else{
//關閉敢闖
}
}
viewModel._loadSuccess.collect {
//載入成功顯示資料
}
viewModel._loadFailed.collect {
//載入失敗邏輯處理
}
這種方式的缺點,相信一眼就可以看出,就是頁面有多少種狀態,就要定義多少個類似的變數,模板程式碼過多,且不利於維護。
所以,UI State集中管理就是將所有狀態寫在一個類中,可以是密封類或者普通類都可以,各有各的好處,這裡我們使用密封類定義,新建MainUiState類,程式碼如下所示:
``` sealed class MainUiState { /* * 正在載入 / object isLoading : MainUiState()
/* * 請求失敗 * @param error 異常日誌 / data class loadError(val error: Exception) : MainUiState()
/* * 請求成功 * @param reqData 返回資料 / data class loadSuccess(val reqData: ReqData):MainUiState()
} ```
我們在MainUiState中提前定義好了UI的各種狀態,怎麼樣,有沒有瞬間回到MVP時代在View中提前定義好各種介面的感覺。
修改ViewModel中的程式碼如下所示:
private val _state = MutableSharedFlow<MainUiState>()
val state: SharedFlow<MainUiState>
get() = _state
這樣只需要定義一個state變數,在Activity監聽如下所示:
``` lifecycleScope.launch { try { viewModel.state.collect { when (it) { MainUiState.isLoading -> {} is MainUiState.loadSuccess -> {} is MainUiState.loadError -> {} } } } catch (e: Exception) {
} } ```
這樣一來,減少了ViewModel的模板程式碼,但是這裡需要注意的是,集中管理是相對的,沒必要把無論是否相關的狀態管理都放在一個密封類中。
當state中的狀態很多時,可能會由於某個屬性改變而頻繁重新整理檢視,開發者沒辦法判斷值是否改變,針對這種情況我們可以使用distinctUntilChanged方法處理,程式碼如下所示:
viewModel.state.distinctUntilChanged().collect {
when (it) {
MainUiState.isLoading -> {}
is MainUiState.loadSuccess -> {}
is MainUiState.loadError -> {}
}
}
使用者意圖管理
在使用MVI之前,重新整理、點選事件操作我們都是在Activity中直接呼叫ViewModel的方法,程式碼如下所示:
``` binding.btnRefresh.setOnClickListener { //重新整理 viewModel.refresh() }
binding.btnLoadData.setOnClickListener { //查詢資料 viewModel.loadData() } ```
其實,上面這種方式也沒有什麼缺陷,在單項資料流模式中,Activity向ViewModel傳送Intent事件,從而ViewModel集中處理使用者操作。可以簡單的理解為使用者事件也統一管理、統一處理。
首先我們定義MainIntent類,定義好頁面中的操作,程式碼如下所示:
``` sealed class MainIntent {
/* * 重新整理 / object refresh : MainIntent()
/ 查詢資料 / object loadData : MainIntent()
} ```
在ViewModel中定義一個userIntent變數用於接收使用者事件,程式碼如下所示:
/**
* 接收事件
*/
private val userIntent = MutableSharedFlow<MainIntent>()
將viewModel中的refresh、loadData方法設為私有,並新建一個dispatch方法用於分發使用者事件,程式碼如下所示:
/**
* 分發使用者事件
* @param viewAction
*/
fun dispatch(viewAction: MainIntent) {
try {
viewModelScope.launch {
userIntent.emit(viewAction)
}
} catch (e: Exception) {
}
}
並在ViewModel中檢測userIntent的變化,觸發對應請求,程式碼如下所示:
init {
viewModelScope.launch {
userIntent.collect {
when (it) {
is MainIntent.refresh -> refresh()
is MainIntent.loadData -> loadData()
else -> {}
}
}
}
}
這樣,在Activity中,只需要傳遞使用者意圖即可,程式碼如下所示:
``` binding.btnRefresh.setOnClickListener { //重新整理 viewModel.dispatch(MainIntent.refresh) }
binding.btnLoadData.setOnClickListener { //查詢資料 viewModel.dispatch(MainIntent.loadData) } ```
這樣一來,將事件管理、狀態轉化都放在了ViewModel中,這樣體現的好處就是保證資料一致性,不通過頁面也可以清晰的看到有哪些事件、狀態。當XML替換為Compose的時候,就可以只注重頁面的實現了?
關於網域層
關於網域層的介紹很少,基本都是按照官方意思概括。因為他是可有可無的,甚至說對一般App來說都是不需要的。但是網域層到底是什麼東西呢?為什麼他是可有可無的呢?這裡我說一下自己的理解。
網域層是位於頁面層和資料層之間的,也就是Activity與Respository層之間的。可以負責封裝複雜的業務邏輯,或者多個ViewModel重複使用的簡單業務邏輯。
我對網域層的理解,類似設計模式中的 ”門面模式“,關於門面模式,後面我會在單獨寫一篇文章介紹。
簡單的說 比如現在有ARespository和BRespository,分別查詢資料A和資料B,在業務A、B模組中需要各自查詢資料A、B,在業務C模組和D模組中都需要將A、B資料通過業務邏輯處理(如拼接、去重等操作)後顯示。此時這部分業務邏輯是沒辦法直接寫在A或BRespository中的,但又是一個重複業務邏輯,所以我們抽取出一個網域層,用於接收A、B層的資料,將資料處理後返回給UI層。同時還可能有其他業務模組的資料來源來自CRespository和ARespository,此時再抽取一個網域層用於單獨處理資料。這樣一來,避免了程式碼重複、將部分重複邏輯抽取到網域層減輕其他層的負擔。
寫在最後
相信看了這篇文章,你對在Android中如何使用MVI有了一定的瞭解,但一定要切記,架構沒有好壞之分,適合專案本身的架構就是好架構~
期待我們下篇文章再見~