Android進階寶典 -- Google官方架構MVI

語言: CN / TW / HK

如果經常看Google官方文檔的夥伴,可能早就發現,Google官方應用架構指南中推薦的架構模式已經不是MVVM,而是一種全新的MVI架構,先把官方的架構圖貼出來

image.png

我們可以看到常見的數據層和UI層還是存在的,中間則是穿插了一個用於做數據層和UI層通信的架構層,類似於MVVM中ViewModel的角色類型,UI層依賴中間層,中間層依賴數據層。

1 MVI架構的優勢

既然Google推出這個架構,那麼這個架構必然是存在自身的優勢,MVVM已經是大眾常見的架構模式,那麼MVI相較於MVVM做了什麼升級呢?

首先我們回顧下MVVM的架構,如下圖所示

image.png

VM層與數據層單向綁定,從數據層獲取數據;UI層和VM層做數據的雙向綁定,通過ViewModel層數據變化驅動UI層更新。

所以MVVM架構是UI層持有VM層的數據做監聽,並刷新UI數據,而MVI呢?我個人認為它和MVVM是非常像的,與MVVM不同的是,MVI是做UI狀態的集中管理,並以單向數據流的形式,將UI的狀態輸出到UI層,UI層根據狀態做相應的處理。

這裏提到了MVI架構的2個特點:\ (1)UI狀態集中管理;\ (2)單向數據流;

在MVVM架構中,並沒有UI狀態這個概念,而是UI層根據數據的變化,做頁面狀態判斷並展示,當然也可以在VM層做狀態管理,但更多的是一個state對應一個LiveData,無法做到集中管理;

第二就是單向數據流,如果做過前端或者IOS的夥伴應該不陌生,單向數據流可以認為是一種設計模式,狀態自上而下,事件自下而上;

image.png 而且UI層更改狀態不會影響數據源的數據,這種優勢在於數據來源是唯一的,針對狀態可以定位問題

2 MVI架構設計

從第一小節中,我們大概知道了MVI的幾個顯著特點,現在我們通過代碼,來一步一步實現一個簡單的MVI架構應用,這裏用聚合數據中的一個接口:查詢天氣預報 http://apis.juhe.cn/simpleWeather/query

2.1 界面層

因為MVI的一個特點就是UI狀態集中管理,因此UI層除了UI Element之外,還需要一個UiState類將所有的狀態集中管理。

image.png

kotlin class WeatherUiState { val isLoading = false //頁面loading val isError = false //頁面錯誤 val weatherData:WeatherRealTime? = null //實時天氣數據 } 在WeatherUiState中,定義了頁面的3種狀態,分別是數據在加載過程中的Loading狀態、加載失敗的狀態error,請求到數據之後展示的頁面數據;

在MVVM架構中,我們經常在UI層監聽ViewModel數據變化,並在UI處理數據實現業務邏輯,那麼在MVI架構中,這種行為是被禁止的,業務邏輯將會放在中間層或者數據層中處理;

那麼在MVI架構中,UI層主要處理界面行為邏輯(即界面邏輯)決定着如何在屏幕上顯示狀態變化。例如使用 Android Resources獲取要在屏幕上顯示的正確文本、在用户點擊某個按鈕時轉到特定屏幕,或者使用Toast彈出提示等

那麼在ViewModel中,需要暴露這個狀態讓UI層去獲取,例如: kotlin class WeatherVM { private val _weatherUiState: MutableStateFlow<WeatherUiState> = MutableStateFlow(WeatherUiState()) val weatherUiState: StateFlow<WeatherUiState> = _weatherUiState.asStateFlow() } 使用MutableStateFlow封裝WeatherUiState,這裏為什麼不用LiveData,稍後再説。

這裏我們想一個問題就是,我們現在是把所有的狀態全部封裝到一起,在ViewModel中只存在單一的數據流,那麼是否需要限制一定使用單一數據流?

其實不是的,關鍵需要看狀態之間的關聯性,例如當頁面加載完成之後,有兩種情況:\ 1 獲取到數據顯示數據\ 2 接口數據獲取失敗,網絡異常 or 服務器異常\

這種狀態其實是強關聯的,封裝在一起是沒有問題;但是如果存在一種狀態與上述的狀態不存在關聯狀態,那麼就可以將這個狀態單獨封裝成一個狀態類,作為另一個數據流存儲在ViewModel中。

2.2 Intent層

這裏就是所謂的I層,試圖事件層,用於接受UI層的事件觸發,向數據層獲取數據。 kotlin binding.btnGet.setOnClickListener { viewModel.getWeather() } 當用户觸發獲取天氣的意圖的時候,請求ViewModel中的一個方法,那麼在這個方法中,就會進行狀態的分發,當發起請求之前,會有loading頁面,然後請求結束之後,loading動畫消失; 會判斷獲取到的數據是否正常,如果不為空,那麼就將數據回調出去;如果數據出現異常,那麼就將錯誤頁面的回調給UI層

kotlin fun getWeather() { viewModelScope.launch { _weatherUiState.update { it.copy(isLoading = true) } val result = WeatherDataSource.getWeather("北京") _weatherUiState.update { it.copy(isLoading = false) } if (result.result?.realtime != null) { _weatherUiState.update { it.copy(weatherData = result.result.realtime) } } else { //異常 _weatherUiState.update { it.copy(isError = true) } } } } 如此一來,UI層的主要作用就是處理這些狀態的回調並展示數據,例如:

kotlin lifecycleScope.launchWhenCreated { viewModel.weatherUiState.collectLatest { state -> Log.e("TAG", "state ==> $state") if (state.isLoading) { //顯示loading binding.csLoading.visibility = View.VISIBLE } else if (!state.isLoading && state.weatherData != null) { //展示數據 binding.csLoading.visibility = View.GONE binding.tvTemperature.text = state.weatherData.temperature } else if (!state.isLoading && state.isError) { //展示錯誤頁面 } } }

對於數據層這裏就不再贅述了,這部分跟MVP、MVVM其實是一致的。

前面我們提到過,為什麼不去使用LiveData,而是採用StateFlow,那麼我們使用LiveData看一下效果,會不會有什麼問題 kotlin fun getWeatherByLiveData() { viewModelScope.launch { weatherLiveData.postValue(WeatherUiState(isLoading = true)) val result = WeatherDataSource.getWeather("北京") weatherLiveData.postValue(WeatherUiState(isLoading = false)) if (result.result?.realtime != null) { weatherLiveData.postValue( WeatherUiState( isLoading = false, weatherData = result.result.realtime ) ) } else { //異常 weatherLiveData.postValue(WeatherUiState(isLoading = false, isError = true)) } } } 我們這裏依然回調了3次狀態,但是UI層只收到了2次狀態的回調,也就是説因為LiveData的特性(回調最新的數據),可能會有部分狀態數據丟失的問題,但是如果使用Flow就不會存在這個問題,因為數據流是不會斷層的。

```kotlin 2022-10-02 21:12:09.162 6356-6356/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=true, isError=false, weatherData=null) 2022-10-02 21:12:09.525 6356-6356/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=WeatherRealTime(temperature=19, humidity=89, info=陰, wid=02, direct=東風, power=2級, aqi=15))

```

3 UiState總結

3.1 不可變性

我們可以發現,在定義頁面狀態的時候,每個屬性值就是不可變的,也就是説整個狀態是不可變的。 kotlin class WeatherUiState { val isLoading = false //頁面loading val isError = false //頁面錯誤 val weatherData:WeatherRealTime? = null //實時天氣數據 } 那麼這樣設計有什麼好處呢?因為狀態不可變,在UI層就無法改變這個狀態的值,因為在UI層改變狀態可能會影響到其他訂閲者的狀態,而且UI層本來就是禁止改變狀態的,除非當前頁面是數據的唯一來源,例如: kotlin binding.btnGet.setOnClickListener { canSubmit = true if(canSubmit){ it.background = resources.getDrawable(R.drawable.ic_launcher_background) } } 這種屬於界面行為邏輯,而不是業務邏輯,這種是可以在UI層做狀態的變化

還有一個優勢在於:UiState始終會存儲當前頁面的最新狀態,即便頁面配置發生改變之後,UiState依然是不變的,這也是跟ViewModel存儲特性結合起來了。

```java 2022-10-02 22:01:46.901 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=null) 2022-10-02 22:01:49.667 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=true, isError=false, weatherData=null) 2022-10-02 22:01:50.096 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=null) 2022-10-02 22:01:50.097 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=WeatherRealTime(temperature=18, humidity=91, info=陰, wid=02, direct=東北風, power=2級, aqi=15))

```

3.2 UiState擴展

在上一小節中,我們看到UI層在監聽狀態變化時,會結合多個狀態來判斷應該展示哪個頁面,這種其實完全沒有必要,因為真正要做到UI層只做頁面展示,這種判斷就可以直接放在UiState中處理即可 kotlin else if (!state.isLoading && state.weatherData != null) { //展示數據 binding.csLoading.visibility = View.GONE binding.tvTemperature.text = state.weatherData.temperature } else if (!state.isLoading && state.isError) { //展示錯誤頁面 } 使用屬性擴展即可

kotlin //是否有數據,正常狀態下 val WeatherUiState.hasData: Boolean get() = !isLoading && weatherData != null kotlin //發生錯誤 val WeatherUiState.error: Boolean get() = !isLoading && isError 簡化後的UI層處理邏輯: kotlin lifecycleScope.launchWhenCreated { viewModel.weatherUiState.collectLatest { state -> Log.e("TAG", "state ==> $state") if (state.isLoading) { //顯示loading binding.csLoading.visibility = View.VISIBLE } else if (state.hasData) { //展示數據 binding.csLoading.visibility = View.GONE binding.tvTemperature.text = state.weatherData?.temperature } else if (state.error) { //展示錯誤頁面 } } }

綜上所述,大家可能對於單向數據流這種模式有一些瞭解,而且為何使用單向數據流,官方也有自己的説法

  • 數據一致性: 界面只有一個可信來源。
  • 可測試性: 狀態來源是獨立的,因此可獨立於界面進行測試。
  • 可維護性: 狀態的更改遵循明確定義的模式,即狀態更改是用户事件及其數據拉取來源共同作用的結果。

數據唯一性,因為對於MVI架構來説,數據就是UiState,每個頁面監聽這個UiState,而且只來源於ViewModel且不可變,不能通過UI層改變其狀態;如果發生了改變,只能是ViewModel推動狀態的改變,所以數據流是單向的,這才是真正的數據驅動UI;

而且可以追本溯源,某個狀態出現問題,就可以直接定位到狀態更新的位置,查明問題的原因。

當然這也是Google最近才推出來的架構模式,目前主流的依然還是MVVM,如果有想嘗試這個架構設計模式(我已經在項目中開始使用了),夥伴們可以一起來討論