【譯】Android Architecture - ViewModel 與 View 的通訊

前言

本文翻譯自【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 之間的通訊變得更加困難。java

正文

痛點

以一個用戶信息修改界面爲例,在請求服務器以前,必須先校驗用戶數據,而 Presenter 或 ViewModel 的職責就是顯示和取消 Loading,以及將校驗或服務器的返回結果展現到界面上。此外,若是一個 Dialog 正在顯示,當配置變動後也應該恢復 Dialog。 react

1_Edit_Profile.png

Presenter 和 ViewModel 不該持有 View 的引用。android

在 MVP 架構中,咱們常常須要定義一些契約類接口(Contract),View 實現 Contract.View 接口,Presenter 實現 Contract.Presenter 接口,在 Presenter 中不持有 Activity/Fragment 的引用,只持有 View 實例,這樣能夠方便地調用 View 接口暴露的方法。 例如 EditProfileContract.ktgit

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 的引用,而是經過 LiveDataRxJava 向 View 層暴露數據。一旦 View 訂閱了 ViewModel,它就開始接收數據更新。這看似很完美,但當 ViewModel 想要更新 View 狀態,好比顯示和取消 Loading,將數據校驗或服務器結果反饋到 UI 界面上,會變得很是困難。github

解決方案

ViewModel 中的 LiveData 或 Observable 越少越好。所以咱們最好找到一種方法,能夠封裝須要傳遞給 View 層的數據和信息。在多數狀況下,ViewModel 須要向 View 層暴露如下三種數據:bash

  • Data
  • Status
  • State 下面將依次介紹。

Data

Data -- 就是須要在 View 上展現的內容,好比用戶信息的 User 實體類,或社交 Feed 流中的列表項。服務器

val user = MutableLiveData<User>()
val feeds = MutableLiveData<List<Feed>>()
複製代碼

Status

Status -- 能夠是任何僅需傳遞一次的信息,如校驗錯誤,網絡異常,或者服務器錯誤。 Status.Kt網絡

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() 方法, 注意:只有一個觀察者會受到更新通知。app

新建一個 SingleLiveEvent 用來向 View 層暴露 Status 數據。 EditProfileViewModel.Kt

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。 EditProfileFragment.Kt

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 層。一種簡單的作法是,咱們能夠建立一個數據類來保存這些狀態。 EditProfileState.Kt

data class EditProfileState(
    var isProgressIndicatorShown: Boolean = false,
    var isCityDialogShown: Boolean = false,
    var isGenderDialogShown: Boolean = false)
複製代碼

而後在 ViewModel 中建立一個 MutableLiveData,用來包裝這個 EditProfileState。因爲 ViewModel 只會暴露 LiveData 給 View 層,所以咱們應該提供 setter 方法,便於 View 更新此狀態。 EditProfileViewModel.kt

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 的顯示和取消。 EditProfileFragment.Kt

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 保持乾淨簡潔。對我來講,StatusState 是一種好的解決方案。

評論中的問題

  • 關於 enum 的使用: -結論:In fact, if you use enums, I don't care. Go ahead

  • 關於使用 DataBinding

    • 能夠使用 DataBinding 解決 VM 和 V 的通訊。

參考

聯繫

我是 xiaobailong24,您能夠經過如下平臺找到我:

相關文章
相關標籤/搜索