一文了解MVI架構,學起來吧~

語言: CN / TW / HK

前言

大約在去年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()     val _isLoding: SharedFlow         get() = isLoding

/**      * 載入成功      /     private val loadSuccess = MutableSharedFlow()     val _loadSuccess: SharedFlow         get() = loadSuccess

/**      * 載入失敗      /     private val loadFailed = MutableSharedFlow()     val _loadFailed: SharedFlow         get() = loadFailed ```

由於不能破壞資料的封裝性,所以我們要定義一個私有的不可變的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有了一定的瞭解,但一定要切記,架構沒有好壞之分,適合專案本身的架構就是好架構~ 

期待我們下篇文章再見~