ViewModel与Activity/Fragments通讯的正确姿势

5,812 阅读2分钟

译文地址:android.jlelse.eu/sending-eve…

在MVVM编程方式中,一般通过从ViewModel中向Activity/fragment发送一个消息来显示Toast或者snack bar。传统的这种方式是有很大弊端的。

我们现在要显示一个Toast,ViewModel决定了什么时候显示什么信息,而具体的显示功能,则由View来实现。

错误的消息处理方式

使用接口或者其他回调

通过定义一个接口,让Activity/Fragment来实现接口。当需要传递消息时,只需要调用接口中的方法。方式简单而又粗暴~~~,但却是最愚蠢的选择。在MVVM中,我们不再通过接口来进行View与ViewModel之间的通讯,而且ViewModel也不应该持有View的引用。

使用LiveData发送消息

将信息放在LiveData中,Activity/Fragment通过监听LiveData来进行消息处理。这种方式简单有效,但却有一些小缺陷。

class MyViewModel() : ViewModel(){
    val networkError = MutableLiveData<String>()
    
}

//inside activity
viewModel.networkError.observe(this, Observer { 
    showToast(it)
})

这样的代码在一些情况下并不能满足我们的需求。

  • 当Activity/Fragment销毁时,ViewModel并未销毁。当横竖屏切换时,消息会重复提示(也许我们已经多次处理了消息了)。如果我们在onViewCreated之后进行监听,情况会更恶劣。每次onDestroryView 进行view的销毁之后,再重新绘制时,都会进行消息的提示。
  • 某些情况下存在多个fragments使用同一个ViewModel。当进行消息通知时,所有的界面可能都会进行消息的处理。显而易见,我们只需要在某一个固定的监听者中进行消息的处理就足够了

使用只有一次通知功能LiveData

有一些LiveData的继承类,实现了只通知监听者一次。采用这种方式能够有效的解决上一个方案中的第一个问题。 相信很多人已经在实际工作中使用这种方式。但是当我们遇到第二个问题的时候,我们根本无法判断到底是哪个fragment监听到了消息。

正确的消息处理姿势

LiveData具有生命感知功能,并且只在View处于激活状态时进行消息的通知。我们通过一层封装,在充分利用LiveData的优势的条件下,实现我们只进行一次消息通知的功能。

open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandledOrReturnNull(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}


class MyViewModel() : ViewModel(){
    val networkError = MutableLiveData<Event<String>>()
    
}

viewModel.networkError.observe(this, Observer { event ->
    event?.getContentIfNotHandledOrReturnNull()?.let {
        showToast(it)
    }
})

通过对消息的明确的处理,所有的监听者都可能获取到多次数据(Activity/Fragment可能进行了销毁并重建),但是Event事件只会被第一个接收到消息的observer处理。