Stories

Detail Return Return

MVVM 成為歷史,Google 全面倒向 MVI - Stories Detail

前言

前段時間寫了一些介紹MVI架構的文章,不過軟件開發上沒有最好的架構,只有最合適的架構,同時眾所周知,Google推薦的是MVVM架構。相信很多人都會有疑問,我為什麼不使用官方推薦的MVVM,而要用你説的這個什麼MVI架構呢?

不過我這幾天查看Android的應用架構指南,發現谷歌推薦的最佳實踐已經變成了單向數據流動 + 狀態集中管理,這不就是MVI架構嗎?看起來Google已經開始推薦使用MVI架構了,大家也有必要開始瞭解一下Android應用架構指南的最新版本了~

總體架構

兩個架構原則

Android的架構設計原則主要有兩個

分離關注點

要遵循的最重要的原則是分離關注點。一種常見的錯誤是在一個 ActivityFragment 中編寫所有代碼。這些基於界面的類應僅包含處理界面和操作系統交互的邏輯。總得來説,ActivityFragment中的代碼應該儘量精簡,儘量將業務邏輯遷移到其它層

通過數據驅動界面

另一個重要原則是您應該通過數據驅動界面(最好是持久性模型)。數據模型獨立於應用中的界面元素和其他組件。
這意味着它們與界面和應用組件的生命週期沒有關聯,但仍會在操作系統決定從內存中移除應用的進程時被銷燬。
數據模型與界面元素,生命週期解耦,因此方便複用,同時便於測試,更加穩定可靠。

推薦的應用架構

基於上一部分提到的常見架構原則,每個應用應至少有兩個層:

  • 界面層 - 在屏幕上顯示應用數據。
  • 數據層 - 提供所需要的應用數據。

您可以額外添加一個名為“網域層”的架構層,以簡化和複用使用界面層與數據層之間的交互

如上所示,各層之間的依賴關係是單向依賴的,網域層,數據層不依賴於界面層

界面層

界面的作用是在屏幕上顯示應用數據,並響應用户的點擊。每當數據發生變化時,無論是因為用户互動(例如按了某個按鈕),還是因為外部輸入(例如網絡響應),界面都應隨之更新,以反映這些變化。
不過,從數據層獲取的應用數據的格式通常不同於UI需要展示的數據的格式,因此我們需要將數據層數據轉化為頁面的狀態
因此界面層一般分為兩部分,即UI層與State HolderState Holder的角色一般由ViewModel承擔

數據層的作用是存儲和管理應用數據,以及提供對應用數據的訪問權限,因此界面層必須執行以下步驟:

  1. 獲取應用數據,並將其轉換為UI可以輕鬆呈現的UI State
  2. 訂閲UI State,當頁面狀態發生改變時刷新UI
  3. 接收用户的輸入事件,並根據相應的事件進行處理,從而刷新UI State
  4. 根據需要重複第 1-3 步。

主要是一個單向數據流動,如下圖所示:

因此界面層主要需要做以下工作:

  1. 如何定義UI State
  2. 如何使用單向數據流 (UDF),作為提供和管理UI State的方式。
  3. 如何暴露與更新UI State
  4. 如何訂閲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 StateNewsItemUiState對象中的bookmarked標記在Activity類中已更新,那麼該標記會與數據層展開競爭,從而產生多數據源的問題。

UI State集中管理的優缺點

MVVM中我們通常是多個數據流,即一個State對應一個LiveData,而MVI中則是單個數據流。兩者各有什麼優缺點?
單個數據流的優點主要在於方便,減少模板代碼,添加一個狀態只需要給data class添加一個屬性即可,可以有效地降低ViewModelView的通信成本
同時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 diffingUiState 對象中的字段越多,數據流就越有可能因為其中一個字段被更新而發出。由於視圖沒有 diffing 機制來了解連續發出的數據流是否相同,因此每次發出都會導致視圖更新。當然,我們可以對 LiveDataFlow使用 distinctUntilChanged() 等方法來實現局部刷新,從而解決這個問題

使用單向數據流管理UI State

上文提到,為了保證UI中不能修改狀態,UI State中的元素都是不可變的,那麼如何更新UI State呢?
我們一般使用ViewModel作為UI State的容器,因此響應用户輸入更新UI State主要分為以下幾步:

  1. ViewModel 會存儲並公開UI StateUI State是經過ViewModel轉換的應用數據。
  2. UI層會向ViewModel發送用户事件通知。
  3. ViewModel會處理用户操作並更新UI State
  4. 更新後的狀態將反饋給UI以進行呈現。
  5. 系統會對導致狀態更改的所有事件重複上述操作。

舉個例子,如果用户需要給新聞列表加個書籤,那麼就需要將事件傳遞給ViewModel,然後ViewModel更新UI State(中間可能有數據層的更新),UI層訂閲UI State訂響應刷新,從而完成頁面刷新,如下圖所示:

為什麼使用單向數據流動?

單向數據流動可以實現關注點分離原則,它可以將狀態變化來源位置、轉換位置以及最終使用位置進行分離。
這種分離可讓UI只發揮其名稱所表明的作用:通過觀察UI State變化來顯示頁面信息,並將用户輸入傳遞給ViewModel以實現狀態刷新。

換句話説,單向數據流動有助於實現以下幾點:

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

暴露與更新UI State

定義好UI State並確定如何管理相應狀態後,下一步是將提供的狀態發送給界面。我們可以使用LiveData或者StateFlowUI 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架構下支持屬性監聽,更加詳細地內容可見:MVI 架構更佳實踐:支持 LiveData 屬性監聽

網域層

網域層是位於界面層和數據層之間的可選層。

網域層負責封裝複雜的業務邏輯,或者由多個ViewModel重複使用的簡單業務邏輯。此層是可選的,因為並非所有應用都有這類需求。因此,您應僅在需要時使用該層。
網域層具有以下優勢:

  1. 避免代碼重複。
  2. 改善使用網域層類的類的可讀性。
  3. 改善應用的可測試性。
  4. 讓您能夠劃分好職責,從而避免出現大型類。

我感覺對於常見的APP,網域層似乎並沒有必要,對於ViewModel重複的邏輯,使用util來説一般就已足夠
或許網域層適用於特別大型的項目吧,各位可根據自己的需求選用,關於網域層的詳細信息可見:https://developer.android.com...

數據層

數據層主要負責獲取與處理數據的邏輯,數據層由多個Repository組成,其中每個Repository可包含零到多個Data Source。您應該為應用處理的每種不同類型的數據創建一個Repository類。例如,您可以為與電影相關的數據創建 MoviesRepository 類,或者為與付款相關的數據創建 PaymentsRepository 類。當然為了方便,針對只有一個數據源的Repository,也可以將數據源的代碼也寫在Repository,後續有多個數據源時再做拆分

數據層跟之前的MVVM架構下的數據層並沒用什麼區別,這裏就不多介紹了,關於數據層的詳細信息可見:https://developer.android.com...

總結

相比老版的架構指南,新版主要是增加了網域層並修改了界面層,其中網域層是可選的,各位各根據自己的項目需求使用。
而界面層則從MVVM架構變成了MVI架構,強調了數據的單向數據流動狀態的集中管理。相比MVVM架構,MVI架構主要有以下優點

  1. 強調數據單向流動,很容易對狀態變化進行跟蹤和回溯,在數據一致性,可測試性,可維護性上都有一定優勢
  2. 強調對UI State的集中管理,只需要訂閲一個ViewState便可獲取頁面的所有狀態,相對 MVVM 減少了不少模板代碼
  3. 添加狀態只需要添加一個屬性,降低了ViewModelView層的通信成本,將業務邏輯集中在ViewModel中,View層只需要訂閲狀態然後刷新即可

當然在軟件開發中沒有最好的架構,只有最合適的架構,各位可根據情況選用適合項目的架構,實際上在我看來Google在指南中推薦使用MVI而不再是MVVM,很可能是為了統一AndroidCompose的架構。因為在Compose中並沒有雙向數據綁定,只有單向數據流動,因此MVI是最適合Compose的架構。

當然如果你的項目中沒有使用DataBinding,或許也可以開始嘗試一下使用MVI,不使用DataBindingMVVM架構切換為MVI成本不高,切換起來也比較簡單,在易用性,數據一致性,可測試性,可維護性等方面都有一定優勢,後續也可以無縫切換到Compose

如果看完本文對你有收穫,請動動你發財的小手,點點贊,你的點贊是我最大的動力。

相關學習視頻推薦:

【2021最新版】Android studio安裝教程+Android(安卓)零基礎教程視頻(適合Android 0基礎,Android初學入門)含音視頻_嗶哩嗶哩_bilibili

Android架構設計原理與實戰——Jetpack結合MVP組合應用開發一個優秀的APP!_嗶哩嗶哩_bilibili

Android進階必學:jetpack架構組件—Navigation_嗶哩嗶哩_bilibili

Android進階系統學習——Jetpack先天優秀的基因可以避免數據內存泄漏_嗶哩嗶哩_bilibili

user avatar segmentfault Avatar teamcode Avatar u_9449786 Avatar wenzhongdejianpan Avatar hejing-michael Avatar uwatechnologies Avatar stars-one Avatar _wss Avatar renzhendezicai Avatar coderdd Avatar leguandeludeng Avatar maomaotou Avatar
Favorites 41 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.