ViewModel:一次性事件反模式
- - -
ViewModel事件行动源于ViewModel, UI应该执行。例如,向用户显示一条消息,或者导航到另一个屏幕,当应用程序状态的变化。
我们指导ViewModel事件固执己见的在两种不同的方式:
- 只要一次性事件起源于ViewModel,ViewModel应该立即处理该事件导致状态更新。ViewModel应该只公开应用程序状态。没有暴露事件减少从视图模型状态意味着ViewModel不是真理的来源来自这些事件的状态;单向数据流(UDF)描述的优点只有消费者,比他们的生产者发送事件。
- 国家应该使用一个可观测的数据夹类型。
在你的应用程序,你可能会暴露UI使用ViewModel事件芬兰湾的科特林通道或其他反应流等SharedFlow
你看过,也许这是一个模式在其他项目。当生产者(视图模型
)比消费者(UI-Compose或观点),可与ViewModel事件中,这些api不保证的交付和处理这些事件。这可能导致bug给开发人员和未来的问题,这也是一个不可接受的用户体验对于大多数应用程序。
你应该立即处理ViewModel事件,导致UI状态更新。试图揭露事件作为一个对象使用通道等反应的解决方案或SharedFlow并不能保证交货和处理事件。
案例研究
这里有一个例子的实现视图模型在一个应用程序的典型的支付流。下面的代码片段中,MakePaymentViewModel
直接告诉UI导航到付款结果屏幕当付款请求的结果回来。我们将使用这个例子中探索为什么这样处理一次性ViewModel事件带来的问题和工程成本的上升。
UI将使用这个事件和相应的导航:
的navigateToPaymentResultScreen
实现上面有多个设计缺陷。
反模式# 1:关于付款完成的状态可以丢失
一个通道并不能保证的交付和处理事件。因此,事件可能会丢失,使UI处于不一致的状态。这可能发生在UI的一个例子(消费者)的背景和停止通道
收集后视图模型
(生产商)发送一个事件。也是表示对其他api,并不是一个可观测的数据夹类型等SharedFlow
,这可能发出事件即使没有消费者听他们。
这是一个反模式,因为支付结果状态在UI层建模不是耐用或原子如果我们考虑它的酸的事务。付款可能成功存储库而言,但是我们从来没有搬到合适的下一个屏幕。
注意:此反模式可以减轻通过使用Dispatchers.Main.immediate
当发送和接收事件。但是,如果这不是强制的线头检查,这个解决方案可以容易出错的开发者很容易忘记。
反模式# 2:告诉UI采取行动
对于一个应用程序,支持多种屏幕尺寸,UI动作执行给定ViewModel事件根据屏幕大小可能不同。例如,案例研究时要导航到付款结果屏幕应用程序上运行的手机;但如果应用在平板电脑上运行,动作可以显示相同的结果在不同的部分屏幕。
ViewModel应该告诉UI什么应用状态和UI应该确定如何来反映。ViewModel不应该告诉它应该采取行动的UI。
反模式# 3:不立即处理的一次性事件
建模和一些事件火和忘记-在飞行中是导致问题。这是难以遵守ACID属性最高,所以可能不能保证数据的可靠性和完整性。状态是、事件发生。事件并不是处理时间越长,问题就变得越困难。ViewModel事件,尽快处理事件,生成一个新的UI状态。
在案例研究中,我们创建了一个事件,表示为一个对象布尔
——,并把它使用相接触通道
:
/ /创建通道事件建模为一个布尔值
val _navigateToPaymentResultScreen = <布尔>频道()/ /触发事件
_navigateToPaymentResultScreen.send (isPaymentSuccessful)
一旦你这样做,你的责任确保只有一次交付和处理。如果你必须模型事件作为一个对象出于某种原因,限制其寿命尽可能短,以便它不会有机会迷路。
处理一次性事件在ViewModel通常可以归结为一个方法调用——例如,更新UI状态。一旦你调用这个方法,你知道它成功完成或者抛出一个异常,你知道它到底发生了一次。
案例研究改进
如果你在这些情况下,重新考虑什么一次性ViewModel事件实际上意味着对你的UI。立即处理它们,减少他们UI状态暴露使用一个可观测的数据等StateFlow
或mutableStateOf
。
UI状态更好的代表UI在给定的时间点,它会给你更多的交付和处理担保,通常更容易测试和持续集成与应用程序的其余部分。
如果你很难找到一个方法来减少一次性ViewModel事件状态,考虑为你的UI事件实际上意味着什么。
在上面的示例中,视图模型应该公开的实际付款应用程序数据——数据在这种情况下,而不是告诉UI动作。下面是更好的表示,ViewModel事件处理和减少,,并使用一个可观测的数据夹类型相接触。
在上面的代码中,事件处理立即通过调用_uiState.update
(# L28)与新paymentResult
数据(# L31);没有为这个事件现在迷路了。事件已经减少到状态,paymentResult
场MakePaymentUiState
反映了支付结果应用程序数据。
,用户界面反应paymentResult
改变并采取相应行动。
注意:如果你的用例的活动没有完成()
和backstack保存在你的视图模型需要公开函数来清除paymentResult
从UiState(即设置字段零
活动开始后),将调用另一个。可以找到一个这样的例子消费事件可以触发状态更新部分的文档。
正如前面提到的,UI层的额外的考虑部分,你可以让UI屏幕与多个流的状态,如果这就是你的用例需要。重要的是,这些流是可观测的数据夹类型。在上面的示例中,一个独特的暴露,因为UI状态流isLoading
国旗和paymentResult
财产高度交织在一起。分离出来可能会导致不一致在ui的例子中,如果isLoading
是真正的
和paymentResult
不是零
。通过一起在同一个UiState类,我们更加意识到不同领域,使UI屏幕的状态,让我们更少的错误。
希望这篇文章帮助你理解的原因为什么我们建议1)一次性ViewModel事件处理的立即和减少他们的状态,和2)暴露状态使用一个可观测的数据夹类型。我们相信这种方法保证加工运输给你更多,通常更容易测试和持续集成与应用程序的其余部分。
免责声明:与其他体系结构指导,把这作为一个准则,它适应您的需求。
关于这个主题的更多信息,请查看UI事件文档。
特别感谢亚当•鲍威尔无休止的讨论、知识和输入他的这篇博客。也,啤酒Stamato和何塞Alcerreca彻底的审查。