今天,我们将讨论如何在MVVM中的Activity / Fragments和ViewModel之间交换事件。 要从ViewModel获取数据,建议在Activity / Fragment中订阅在ViewModel中找到的LiveData数据。 但是如何发送单个(不仅是)事件,例如显示通知或打开另一个片段?

大家好!
我叫Alexei,我是Home Credit Bank的一名Android开发人员。
在本文中,我将分享我们从ViewModels向视图发送和接收事件的方式(活动/片段)。
在我们的应用程序“家庭信用银行的分期付款商品”中,我们使用了片段,因此我们将讨论它们,但是所有内容都与活动有关。我们想要什么?
我们有一个Fragment,其中包括几个ViewModel,数据由
DataBinding
。 所有用户事件都接收ViewModel。 如果该事件是一个导航事件:您需要打开另一个片段/活动,显示
AlertDialog
,
Snackbar
,系统对Permissions的请求等,则应在片段中执行这样的事件。
到底是什么问题?
视图和ViewModels生命周期是不相关的。 与侦听器绑定是不可能的,因为ViewModel应该不了解有关片段的任何信息,也不应包含指向片段的链接,否则,正如您所知,内存将开始“
欢喜 ”。
片段和ViewModels之间交互的标准方法是订阅位于ViewModel中的
LiveData
。 由于这种方法未考虑事件是否已经完成,因此无法直接通过
LiveData
传输事件。
存在哪些解决方案:
1.使用
SingleLiveEvent
优点:该事件执行一次。
缺点:一个事件-一个
SingleLiveEvent
。 对于大量事件,N个事件对象出现在ViewModel中,每个事件对象都必须在片段中进行预订。
2.一个很好的
例子 。
优点:一个事件也执行一次,您可以将数据从viewModel传输到片段。
缺点:需要事件中的数据,但是如果您需要在没有数据的情况下执行事件
(val content: T)
,则需要创建另一个类。 它不能解决一次执行一种类型的事件的问题(事件本身只执行一次,但是这种类型的事件将被执行与从ViewModel运行它一样多的次数)。 例如,N个请求异步进行,但是没有网络。 每个请求都会返回一个网络错误,并将拉入N个有关网络错误的事件的片段,N个警报将在该片段中打开。 用户不会批准这样的决定:)。 我们应该向他显示一条错误消息。 换句话说,这种类型的事件应执行一次。
解决方案
我们以
SingleLiveEvent
的想法为基础保存事件处理信息。
定义可能的事件类型
enum class Type { EXECUTE_WITHOUT_LIMITS,
创建事件基类NavigationEvent
isHandled
指示是否接收到事件(如果在片段中接收到Observer,我们认为该事件已执行)。
open class NavigationEvent(var isHandled: Boolean = false, var type: Events.Type)
创建一个Emitter
类- Emitter
事件发射器类继承自
LiveData
<
NavigationEvent
>。 此类将在ViewModel中用于调度事件。
class Emitter : MutableLiveData<NavigationEvent>() { private val waitingEvents: ArrayList<NavigationEvent> = ArrayList() private var isActive = false override fun onInactive() { isActive = false } override fun onActive() { isActive = true val postingEvents = ArrayList<NavigationEvent>() waitingEvents .forEach { if (hasObservers()) { this.value = it postingEvents.add(it) } }.also { waitingEvents.removeAll(postingEvents) } } private fun newEvent(event: NavigationEvent, type: Type) { event.type = type this.value = when (type) { Type.EXECUTE_WITHOUT_LIMITS, Type.EXECUTE_ONCE -> if (hasObservers()) event else null Type.WAIT_OBSERVER_IF_NEEDED, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (hasObservers() && isActive) event else { waitingEvents.add(event) null } } } } fun clearWaitingEvents() = waitingEvents.clear() }
isActive
我们需要了解是否至少有一个
Observer
订阅了
Emitter
。 如果订户已经出现并且等待他的事件已经累积,我们将发送这些事件。 一个重要的澄清:发送事件不是通过
this.postValue(event)
而是通过setter
this.value = event
是必要的。 否则,订阅者将仅收到列表中的最后一个事件。
发送新事件的方法
newEvent(event, type)
接受两个参数-实际上,事件本身和此事件的类型。
为了不记住所有类型的事件(长名称),我们将创建单独的公共方法,这些方法仅接受事件本身:
class Emitter : MutableLiveData<NavigationEvent>() { … fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS) fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE) fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED) fun waitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE) }
正式地,您已经可以在ViewModel中订阅
Emitter
并接收事件,而不管它们的处理方式(无论事件是否已经处理)。
创建一个事件观察者类EventObserver
class EventObserver(private val handlerBlock: (NavigationEvent) -> Unit) : Observer<NavigationEvent> { private val executedEvents: HashSet<String> = hashSetOf() fun clearExecutedEvents() = executedEvents.clear() override fun onChanged(it: NavigationEvent?) { when (it?.type) { Type.EXECUTE_WITHOUT_LIMITS, Type.WAIT_OBSERVER_IF_NEEDED -> { if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) } } Type.EXECUTE_ONCE, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } } } } } }
该Observer接受高阶函数作为输入-事件处理将以片段形式编写(以下示例)。
clearExecutedEvents()
方法,用于清除已执行的事件(那些本应执行一次的事件)。 更新屏幕时必需,例如,在
swipeToRefresh()
。
好吧,实际上,这个观察者订阅了主要的
onChange()
方法,该方法在接收到新的发射器数据时发生。
如果事件具有无限次数的执行类型,则我们检查事件是否已执行并对其进行处理。 我们执行该事件,并指示已接收并处理该事件。
if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) }
如果该事件属于应该执行一次的类型,则检查该事件的类是否在哈希表中。 如果不是,请执行该事件并将该事件的类添加到哈希表中。
if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } }
但是如何在事件内部传输数据?
为此,将
MyFragmentNavigation
接口,该接口将包含从
NavigationEvent()
继承的类。 在下面创建带有或不带有传递参数的各种类。
interface MyFragmentNavigation { class ShowCategoryList : NavigationEvent() class OpenProduct(val productId: String, val productName: String) : NavigationEvent() class PlayVideo(val url: String) : NavigationEvent() class ShowNetworkError : NavigationEvent() }
在实践中如何运作
从ViewModel发送事件:
class MyViewModel : ViewModel() { val emitter = Events.Enitter() fun doOnShowCategoryListButtonClicked() = emitter.emitAndExecute(MyNavigation.ShowCategoryList()) fun doOnPlayClicked() = emitter.waitAndExecuteOnce(MyNavigation.PlayVideo(url = "https://site.com/abc")) fun doOnProductClicked() = emitter.emitAndExecute(MyNavigation.OpenProduct( productId = "123", productTitle = " Samsung") ) fun doOnNetworkError() = emitter.emitAndExecuteOnce(MyNavigation.ShowNetworkError()) fun doOnSwipeRefresh(){ emitter.clearWaitingEvents() ..
片段中接收事件:
class MyFragment : Fragment() { private val navigationEventsObserver = Events.EventObserver { event -> when (event) { is MyFragmentNavigation.ShowCategoryList -> ShowCategoryList() is MyFragmentNavigation.PlayVideo -> videoPlayerView.loadUrl(event.url) is MyFragmentNavigation.OpenProduct -> openProduct(id = event.productId, name = event.otherInfo) is MyFragmentNavigation.ShowNetworkError -> showNetworkErrorAlert() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState)
从本质上讲,我们仅基于
LiveData
得到了Rx-
BehaviorSubjec
ta和
EventBus
-a的
LiveData
,其中
Emitter
可以在艺术家观察者出现之前收集事件,观察者可以监视事件的类型,并且在必要时仅调用一次。
欢迎在评论中提出建议。
链接到源 。
来自家庭信用银行的分期付款计划 。