前言
在 使用 Jetpack 组件的 MVVM 架构项目开发中,View (Activity / Fragement) 通常使用 LiveData 这种可观察数据来 跟 ViewMdoel 通讯。这种机制通常对于用来做数据「展示」非常有效(例如展示用户姓名、头像等)
image.png但是有些数据是只应该被 「消费一次」,例如展示一次 toast,一次界面跳转或者 Dialog 展示。这种数据准确来说是属于 「事件」
image.png我们建议把 Event(事件) 当作 Status(状态) 一部分。在本文中,我们将介绍一些常见的错误和推荐的方法。
技术方案分析 & 对比
以用户登陆场景为例, 在登陆界面 LoginActivity 点击登录按钮, 执行 ViewModel 中的 doLoginRequest, 然后将登陆结果存在 LiveData 中, LoginActivity 监听 这个 LiveData 做界面跳转。
❌ BAD 用法 1
class LoginModel : ViewModel {
private val _loginResult = MutableLiveData<Boolean>()
val loginResult : LiveData<Boolean>
get() = _loginResult
fun doLoginRequest() {
//do login networl request
_loginResult.value = true
}
}
//In the View (activity or fragment):
loginModel.navigateToDetails.observe(this, Observer {
//login success, jump to HomeActivity
if (it) {
startActivity(HomeActivity...)
}
}
)
问题: _ loginResult 中的值在很长时间内保持为 true ,导致不可能再返回到登录页面。我们一步一步来复现这个问题:
1、 用户在 LoginActivity 点击登录按钮跳转到 HomeActivity
2、 用户按返回键回到LoginActivity
3、 此时旋转屏幕
4、 观察者变为可见状态,但是由于ViewModel的 loginResult 仍为 true,LoginActivity又会启动 HomeActivity
❌ Better 做法 2 在观察者中重置 LiveData 值
class LoginModel : ViewModel {
private val _loginResult = MutableLiveData<Boolean>()
val loginResult : LiveData<Boolean>
get() = _loginResult
fun doLoginRequest() {
//do login networl request
_loginResult.value = true
}
fun navigateToHomeHandled() {
_loginResult.value = false
}
}
//In the View (activity or fragment):
loginModel.navigateToHome.observe(this, Observer {
//login success, jump to HomeActivity
if (it) {
//跳转之前重置 LiveData的值
loginModel.navigateToHomeHandled()
startActivity(HomeActivity...)
}
}
)
问题:这种方法的问题在于有一些样板文件(ViewModel 中对于每个Event 都添加了一个新方法) ,并且容易出错, 观察者很容易忘记对 ViewModel 的调用。
✔️ OK: 使用 SingleLiveEvent
SingleLiveEvent 作为适用于该特定场景的解决方案。这是一个只会发送一次更新的 LiveData。
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
@MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
}
class LoginModel : ViewModel {
private val _loginResult = SingleLiveEvent<Any>()
val loginResult : LiveData<Any>
get() = _loginResult
fun doLoginRequest() {
//do login network request
...
_loginResult.call()
}
}
//In the View (activity or fragment):
loginModel.navigateToHome.observe(this, Observer {
//login success, do something
)
loginModel.navigateToHome.observe(this, Observer {
//由于上面其他observer观察了数据,导致这里可能不会被执行
startActivity(HomeActivity...) }
)
问题:
SingleLiveEvent 的问题在于它只限于一个观察者。如果无意中添加了多个观察者,那么只会调用一个,并且不能保证是哪一个观察者得到调用。
✔️ 推荐: 使用 Event Wrapper
在这种方法中,我们可以明确地管理事件是否已被处理,从而减少错误。
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): T = content
}
class LoginModel : ViewModel {
private val _loginResult = MutableLiveData<Event<Boolean>>()
val loginResult : LiveData<Any>
get() = _loginResult
fun doLoginRequest() {
//do login network request
...
_loginResult.value = Event(true)
}
}
//In the View (activity or fragment):
loginModel.navigateToHome.observe(this, Observer {
//login success, jump to HomeActivity
//只有事件从未被处理时才会有值
it.getContentIfNotHandled()?.let {
startActivity(DetailsActivity...)
}
})
特点: 这个方法将事件作为状态的一部分进行建模: 它们现在只是一条消息,不管是否已经被使用。允许多个观察者观察,用户可以使用 getContentIfNotHandled ()或 peekContent ()来决定做什么样的业务处理。
image.png总结
本文从代码设计、易用性、功能支持 等角度分析了LiveData 用于处理 「事件」时的一些技术方案的对比, 推荐使用 EventWrapper 这种 最佳实践 的方式来对 LiveData 的 事件做处理。
网友评论