什么是一次性事件
在应用中,你可能会使用过 Kotlin Channels 或其他响应式流(例如SharedFlow
)向 UI 公开 ViewModel
事件,或者可能你在其他项目中看到过此模式。当生产者(ViewModel
)比消费者(Compose UI
或 View
)生命周期更长时,这些 API 不能保证这些事件的传递和处理。这可能会给开发人员带来错误和未知的问题,而且对于大多数应用来说这也是不可接受的用户体验。
ViewModel
事件是 UI 应该执行的源自 ViewModel
的操作。例如,向用户显示一个消息,或者在应用程序状态发生变化时导航到不同的页面。
我们对 ViewModel
事件的处理有两种不同的建议方式:
- 每当
ViewModel
中发起一次性事件时,ViewModel 应立即处理该事件,从而触发状态更新。ViewModel
应该只公开应用的状态。公开尚未从ViewModel
简化为状态的事件意味着ViewModel
不是从这些事件派生的状态的真实来源;单向数据流 (UDF) 描述了仅向比生产者生命周期更长的消费者发送事件的优点。 - 状态应使用可观察的数据类型进行公开。
案例分析
下面是在应用程序的典型支付流程中实现 ViewModel
的示例。在下面的代码片段中,当支付请求结果返回时,MakePaymentViewModel
直接告诉 UI 导航到支付结果页面。我们将使用此示例来探讨为什么处理此类一次性 ViewModel
事件会带来问题和更高的工程成本。
class MakePaymentViewModel(...) : ViewModel() {
val uiState: StateFlow<MakePaymentUiState> = /* ... */
// ⚠️⚠️ DO NOT DO THIS!! ⚠️⚠️
// This one-off ViewModel event hasn't been handled nor reduced to state
// Boolean represents whether or not the payment was successful
private val _navigateToPaymentResultScreen = Channel<Boolean>()
// `receiveAsFlow` makes sure only one collector will process each
// navigation event to avoid multiple back stack entries
val navigateToPaymentResultScreen = _navigateToPaymentResultScreen.receiveAsFlow()
// Protecting makePayment from concurrent callers
// If a payment is in progress, don't trigger it again
private var makePaymentJob: Job? = null
fun makePayment() {
if (makePaymentJob != null) return
makePaymentJob = viewModelScope.launch {
try {
_uiState.update {
it.