LiveData 处理事件最佳实践

前言

在 使用 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()

   val loginResult : LiveData
       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()

   val loginResult : LiveData
       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 : MutableLiveData() {
    private val mPending = AtomicBoolean(false)
    override fun observe(owner: LifecycleOwner, observer: Observer) {
        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()

   val loginResult : LiveData
       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 的问题在于它只限于一个观察者。如果无意中添加了多个观察者,那么只会调用一个,并且不能保证是哪一个观察者得到调用。

image.png

✔️ 推荐: 使用 Event Wrapper

在这种方法中,我们可以明确地管理事件是否已被处理,从而减少错误。

open class Event(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>()

   val loginResult : LiveData
       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 的 事件做处理。

image.png

你可能感兴趣的:(LiveData 处理事件最佳实践)