Android架构:ViewModel与View之间的通信

阅读 1389
收藏 38
2018-08-29
原文链接:www.jianshu.com

背景

自从Google在I/O上宣布架构组件以来,网上已经有很多关于MVVM架构的讨论,所以许多喜欢Presenters的开发人员已经开始接受ViewModel世界,使用ViewModels可以减少样板代码,在配置更改期间管理数据,使得在多个片段之间共享数据变得容易。然而,它跟View之间的通信就变的更加难了

问题

我们举一个编辑配置文件屏幕的例子。在发送到服务器之前,用户数据必须进行数据验证,这意味着Presenter / ViewModel应该能够向View显示/隐藏提示告知用户。此外,如果在配置更改期间,那么也应该通知View。


1-Atiy0-oRTPDR4GGwjw5cow.png

Presenters和ViewModel不应包含对视图的引用。

对于Presenter类,我们通常定义某种契约接口,其中View实现Contract.view和Presenter实现Contract.presenter。现在,Presenter可以轻松地在View上调用方法,而无需任何activity/fragment 实例。

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)
   }
}

另一方面,ViewModel与View关系是非常松散的。我们通常使用LiveData或RxJava公开数据。一旦View订阅了ViewModel,它就会开始接收更新。如果是将数据传递给Views时,这方式是非常优秀的。但是当ViewModel需要传达View的状态、进度指示器状态、验证/服务器错误提示等等,会出现问题了。

解决方法

LiveData / Observable越少越好。所以我们正在寻找的是一种聚合需要传递给View的信息的方法。在大多数情况下,ViewModel需要传达三件事:
一:Data →这是需要在View上显示的内容,大多数代码示例都显示了如何将数据从ViewModel传递到View。
二、Status→这是传给View的错误状态如验证错误、服务器错误等等
三、State →UI状态,如进度指示器,对话框,每次开始观察ViewModel数据时都应传递给View。我们只需创建一个数据类来保存状态。

为了简洁起见,Data 我就不讲了大家都知道,我将其遗漏,主要讲讲 status 和state的设计

对于status的设计
enum class Status {
   SUCCESS,
   ERROR,
   NO_NETWORK,
   EMPTY_FIRST_NAME,
   EMPTY_LAST_NAME,
   EMPTY_CITY,
   INVALID_URI
}

LiveData 不提供任何开箱的接口,但是创建一个名为SingleLiveEvent的实例,将LiveData状态公开给View层。可以参考Google的例子SingleLiveEvent

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只需要观察状态LiveData并根据不同的状态/错误执行操作,在这个例子中,我们可以轻松地为每个错误显示不同的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的设计

每次开始观察ViewModel数据时都应传递给View。我们只需创建一个数据类来保存状态。

data class EditProfileState(
   var isProgressIndicatorShown: Boolean = false,
   var isCityDialogShown: Boolean = false,
   var isGenderDialogShown: Boolean = false)

在ViewModel中将状态创建为MutableLiveData。由于我们只将LiveData暴露给View层,我们还应该为View提供setter来更新它的状态。

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
}

根据状态数据在View图层中显示或隐藏City / Gender对话框。

总结

聚合信息(加载status,UIstate,errors)将有助于保持ViewModel的精简和清洁。、

推荐一下:
Android博客周刊 :每周分享国内外热门技术博客以及优秀的类库、Google视频、面试经历。
最新源码汇总:每周分享新的开源代码,有效果图,更直观。
请关注公众号,有惊喜。

评论