【譯】Android Architecture - ViewModel 與 View 的通訊
前言
本文翻譯自【 ofollow,noindex">Android Architecture: Communication between ViewModel and View 】,介紹了 MVVM 架構中 VM 與 V 的通訊。感謝作者 Shashank Gupta 。水平有限,歡迎指正討論。
自從 Google 在去年 I/O 大會發布 Architecture Components 以來, MVVM 架構已經成為一種趨勢。很多之前熟練於 MVP 架構的開發者,現在也慢慢開始接受並使用 MVVM 架構了。相比於 Presenter,使用 ViewModel 有以下好處:減少很多樣板程式碼,配置變更時自動恢復資料,可以輕鬆地在多個 Fragment 之間共享資料。然而,ViewModel 與 View 之間的通訊變得更加困難。
正文
痛點
以一個使用者資訊修改介面為例,在請求伺服器之前,必須先校驗使用者資料,而 Presenter 或 ViewModel 的職責就是顯示和取消 Loading,以及將校驗或伺服器的返回結果展示到介面上。此外,如果一個 Dialog 正在顯示,當配置變更後也應該恢復 Dialog。

1_Edit_Profile.png
Presenter 和 ViewModel 不應持有 View 的引用。
在 MVP 架構中,我們經常需要定義一些契約類介面( Contract
),View 實現 Contract.View
介面,Presenter 實現 Contract.Presenter
介面,在 Presenter 中不持有 Activity/Fragment 的引用,只持有 View 例項,這樣可以方便地呼叫 View 介面暴露的方法。
例如 EditProfileContract.kt
:
interface EditProfileContract { interface view { fun setProgress(show: Boolean) fun showEmptyFirstNameError() fun showEmptyLastNameError() } interface presenter { fun saveProfile(firstName: String, lastName: String, bio: String, email: String, city: City, gender: String) } }
但是,在 MVVM 架構中,ViewModel 不再持有 View 的引用,而是通過 LiveData 或 ReactiveX/RxJava" target="_blank" rel="nofollow,noindex">RxJava 向 View 層暴露資料。一旦 View 訂閱了 ViewModel,它就開始接收資料更新。這看似很完美,但當 ViewModel 想要更新 View 狀態,比如顯示和取消 Loading,將資料校驗或伺服器結果反饋到 UI 介面上,會變得非常困難。
解決方案
ViewModel 中的 LiveData 或 ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/Observable.java" target="_blank" rel="nofollow,noindex">Observable 越少越好。因此我們最好找到一種方法,可以封裝需要傳遞給 View 層的資料和資訊。在多數情況下,ViewModel 需要向 View 層暴露以下三種資料:
- Data
- Status
- State
下面將依次介紹。
Data
Data -- 就是需要在 View 上展示的內容,比如使用者資訊的 User 實體類,或社交 Feed 流中的列表項。
val user = MutableLiveData<User>() val feeds = MutableLiveData<List<Feed>>()
Status
Status -- 可以是任何僅需傳遞一次的資訊,如校驗錯誤,網路異常,或者伺服器錯誤。
enum class Status { SUCCESS, ERROR, NO_NETWORK, EMPTY_FIRST_NAME, EMPTY_LAST_NAME, EMPTY_CITY, INVALID_URI }
LiveData
沒有提供任何開箱即用的方法,但在 Google 的官方示例中,有一個 SingleLiveEvent 的實現,可以解決這個問題。
一個生命週期感知的被觀察者,僅在訂閱後傳送新的更新,常用於導航和 Snackbar 訊息等事件。
這可以避免一些常見問題:在配置變更(如螢幕旋轉)期間,如果觀察者處於活動動態, SingleLiveEvent
將會發送更新事件。
它繼承於 MutableLiveData
,是一個被觀察者,即使對外暴露了 SingleLiveEvent#setValue()
或 SingleLiveEvent#call()
方法,
注意:只有一個觀察者會受到更新通知。
新建一個 SingleLiveEvent
用來向 View 層暴露 Status 資料。
private val status = SingleLiveEvent<Status>() fun getStatus(): LiveData<Status> { return status } fun handleImage(intent: Intent?) { intent?.data?.let { avatar.value = it.toString() } ?: run { status.value = Status.INVALID_URI } }
View 只關心 Status 資料,並根據不同的狀態或錯誤執行對應的邏輯。如下例項,我們能很方便地根據每個錯誤顯示不同的 Toast 或 Snackbar。
viewModel.getStatus().observe(this, Observer { handleStatus(it) }) private fun handleStatus(status: Status?) { when (status) { Status.EMPTY_FIRST_NAME -> Toast.makeText(activity, "Please enter your first name!", Toast.LENGTH_SHORT).show() Status.EMPTY_LAST_NAME -> Toast.makeText(activity, "Please enter your last name", Toast.LENGTH_SHORT).show() Status.EMPTY_CITY -> Toast.makeText(activity, "Please choose your home city", Toast.LENGTH_SHORT).show() Status.INVALID_URI -> Toast.makeText(activity, "Unable to load the photo", Toast.LENGTH_SHORT).show() Status.SUCCESS -> { startActivity(HomeFragment.newIntent(activity)) activity.finish() } else -> Toast.makeText(activity, "Something went wrong, please try again!", Toast.LENGTH_SHORT).show() } }
State
State -- 即 UI 狀態,比如載入進度條和 Dialog 等,每次開始訂閱 ViewModel 的資料時,ViewModel 應該把這些 UI 狀態通知給 View 層。一種簡單的做法是,我們可以建立一個數據類來儲存這些狀態。
data class EditProfileState( var isProgressIndicatorShown: Boolean = false, var isCityDialogShown: Boolean = false, var isGenderDialogShown: Boolean = false)
然後在 ViewModel 中建立一個 MutableLiveData
,用來包裝這個 EditProfileState
。由於 ViewModel 只會暴露 LiveData 給 View 層,因此我們應該提供 setter
方法,便於 View 更新此狀態。
private val state = MutableLiveData<EditProfileState>() fun getState(): LiveData<EditProfileState> { return state } fun setProgressIndicator(isProgressIndicatorShown: Boolean) { state.value?.isProgressIndicatorShown = isProgressIndicatorShown } fun setCityDialogState(isCityDialogShown: Boolean) { state.value?.isCityDialogShown = isCityDialogShown } fun setGenderDialogState(isGenderDialogShown: Boolean) { state.value?.isGenderDialogShown = isGenderDialogShown }
最後,根據上面的 State 狀態資料,決定 Dialog 的顯示和取消。
viewModel.getState().observe(this, Observer { handleState(it) }) private fun handleState(state: EditProfileState?) { if (state?.isCityDialogShown == true) { showCitySelectionDialog() return } if (state?.isGenderDialogShown == true) { showGenderSelectionDialog() return } }
總結
封裝諸如 loading 狀態,UI 狀態或伺服器錯誤等資訊,可以讓 ViewModel 保持乾淨簡潔。對我來說, Status
和 State
是一種好的解決方案。
評論中的問題
-
關於 enum 的使用:
-結論: In fact, if you use enums, I don't care. Go ahead
-
關於使用 DataBinding :
- 可以使用 DataBinding 解決 VM 和 V 的通訊。
參考
- Android Architecture: Communication between ViewModel and View
- Official documentation
- The price of ENUMs (100 Days of Google Dev)
- Modern Android development: Android Jetpack, Kotlin, and more (Google I/O 2018)
聯絡
我是 xiaobailong24 ,您可以通過以下平臺找到我: