前言
本文翻译自【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。
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 或 RxJava 向 View 层暴露数据。一旦 View 订阅了 ViewModel,它就开始接收数据更新。这看似很完美,但当 ViewModel 想要更新 View 状态,比如显示和取消 Loading,将数据校验或服务器结果反馈到 UI 界面上,会变得非常困难。
解决方案
ViewModel 中的 LiveData 或 Observable 越少越好。因此我们最好找到一种方法,可以封装需要传递给 View 层的数据和信息。在多数情况下,ViewModel 需要向 View 层暴露以下三种数据:
- Data
- Status
- State
下面将依次介绍。
Data
Data -- 就是需要在 View 上展示的内容,比如用户信息的 User 实体类,或社交 Feed 流中的列表项。
val user = MutableLiveData()
val feeds = MutableLiveData>()
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()
方法,
注意:只有一个观察者会受到更新通知。
新建一个 SingleLiveEvent
用来向 View 层暴露 Status 数据。
EditProfileViewModel.Kt:
private val status = SingleLiveEvent()
fun getStatus(): LiveData {
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()
fun getState(): LiveData {
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 保持干净简洁。对我来说,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,您可以通过以下平台找到我:
- Github: https://github.com/xiaobailong24
- : http://www.jianshu.com/u/3dac2ad17b48
- 掘金: https://juejin.im/user/59413cdd61ff4b006cbd8a8a