美文网首页
LiveData 处理事件最佳实践

LiveData 处理事件最佳实践

作者: 北疆小兵 | 来源:发表于2022-04-22 12:42 被阅读0次

    前言

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

    image.png

    ✔️ 推荐: 使用 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 的 事件做处理。

    image.png

    相关文章

      网友评论

          本文标题:LiveData 处理事件最佳实践

          本文链接:https://www.haomeiwen.com/subject/usxnertx.html