在Android开发中,将一次性事件从ViewModel层安全地发送到UI层似乎需要进行长时间的研究才能弄清楚。开个玩笑,实际上在这个简单的事情上似乎没有一个“真正”的共识。每种方法都有一些陷阱、可疑的反模式、许多样板代码,或者在边缘情况下根本不起作用。
// viewModel layer
class MyViewModel: ViewModel() {
private val _eventWithChannel = Channel<Event>()
val eventFlowFromChannel = _eventWithChannel.receiveAsFlow()
}
sealed class Event {
object NavigateToAnotherScreen: Event()
}
// compose layer
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// switch to Dispatchers.Main.immediate maaaaaaaybe a bit overkill
withContext(Dispatchers.Main.immediate) {
viewModel.eventFlowFromChannel.collect {
// handle one-time event
}
}
}
}
为什么不增加一些混乱呢?下面来介绍一种新的方法。
简介
在Google指南和ViewModel中,单次事件反模式在这个问题上是有意见的。简单来说,一次性事件应该是ViewModel中包含的全面UI状态的一部分,并在UI层进行相应处理。将事件分离到它们自己的流中是不推荐的。
但我认为这对于绝大多数情况来说有点过分。它还引入了在适当时间处理/重置状态的手动步骤,从而导致潜在的失败点,否则这些问题根本不存在。
有时,你只需要一种简单有效且可单元测试的方法。难道这真的需要这么复杂吗?
StateFlow/Compose State
它们并不是为了直接处理一次性事件而设计的,因为它们是可观察的状态持有者。一个很好的例子是发送一个事件以导航到另一个屏幕,同时当前屏幕保留在后退栈中。
当按下返回按钮时,最后一个事件将再次被读取,导致不断前往另一个屏幕的尴尬循环!可以通过更多的工作来避免这种情况,如Google指南中所述。
如果你对此感兴趣,可以在这里找到更多细节https://developer.android.com/topic/architecture/ui-layer/events#handle-viewmodel-events
。
SharedFlow
// viewModel layer
class MyViewModel: ViewModel() {
private val _eventWithSharedFlow = MutableSharedFlow<Event>()
val eventWithSharedFlow: SharedFlow<Event> = _eventWithSharedFlow
}
// compose layer
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.eventWithSharedFlow.collect {
// handle one-time event
}
}
}
SharedFlow似乎是流式一次性事件的完美选择。意思是:
-
1. 事件被发送到流中。
-
2. 在Compose层的收集器接收它并对其进行处理。
-
3. 一旦被消耗,该事件将不再被看到,例如在旋转屏幕时。
听起来很完美,对吧?但事实并非如此。在UI层中,流式收集应该仅在生命周期处于STARTED状态以上时执行,以避免浪费资源。
一个很好的例子是collectAsStateWithLifecycle/repeatOnLifecycle
扩展函数。
想象一下,当UI处于PAUSED状态时,事件被发送到SharedFlow以导航到另一个屏幕。结果是什么呢?收集器将不在那里接收它,事件永远丢失。用户永远没有被重定向到下一个屏幕。现在我们有了一个不恰当的状态和一条1星级评论。
选项2 - StateFlow + SingleLiveEvent - 也可以吧?
一次性事件自从LiveData的时代以来就一直是一个热门话题。这个类在95%的应用程序中可能存在。
data class SingleLiveEvent<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 getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
}
LiveData已经被弃用了,所以我们尝试将它与StateFlow一起使用。
class MyViewModel: ViewModel() {
private val _stateflowSingleLiveEvent = MutableStateFlow<SingleLiveEvent<Event>?>(null)
val stateflowSingleLiveEvent: StateFlow<SingleLiveEvent<Event>?> = _stateflowSingleLiveEvent
}
// compose layer
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stateflowSingleLiveEvent.collect { singleLiveEvent ->
singleLiveEvent?.getContentIfNotHandled()?.let { event ->
when (event) {
// handle one-time event
}
}
}
}
这个方法起初是可行的,即使看起来有点不太好。 人们可能会期望由于StateFlow
的性质,在旋转等情况下会重复看到相同的事件,但getContentIfNotHandled
函数可以防止这种情况发生。
然而,它也存在一个问题。
在内部,StateFlow
在更新时始终检查新值和旧值之间的相等性。如果它们相同,新值将不会被发送到收集器。因此,无法直接使用数据类data class SingleLiveEvent
,这真是遗憾,因为数据类在单元测试时非常方便。
必须将其转换为常规类。
class SingleLiveEvent<out T>(private val content: T) {
// same as before..
}
为什么要这样呢?想象一种情况,你需要连续两次发送相同的事件。这并不罕见!很多时候,用户会一次又一次地尝试某事,导致完全相同的错误发生(例如toast)。
-
• 第一次更新流应该正常工作。
-
• 第二次更新流时,事件完全相同。
-
• StateFlow在内部将旧值和新值视为完全相同(它是一个数据类,它不检查引用相等性,而是检查其中的内容)。 -由于它们相同,将跳过发送第二个事件。第二个事件就此丢失。
但是,有一个解决方法。可以为每个事件添加唯一的时间戳/ID。
data class SingleLiveEvent<out T>(
private val content: T,
private val id: String = UUID.randomUUID().toString()
) {
// same as before
}
虽然不是很理想,但是它能够解决问题。
选项3 - Channel + Flow
这次不包括Dispatchers.Main.immediate
:
// viewModel layer
class MyViewModel: ViewModel() {
private val _eventWithChannel = Channel<Event>()
val eventFlowFromChannel = _eventWithChannel.receiveAsFlow()
}
// compose layer
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.eventFlowFromChannel.collect {
// handle one-time event
}
}
}
这种方法的优点是:
-
1. 即使在事件被发送时没有收集器存在,它也可以工作。事件将保留在流中,直到收集器返回到STARTED状态以消费它。
-
2. 纯粹的Flow,无需引入LiveData的依赖。
-
3. 最少的样板代码。
-
4. 易于单元测试和推理
-
5. 不需要额外的逻辑
但也有一些缺点:
-
• 不适用于多个并发收集器。
-
• 错过某个活动的可能性很小。
此外,使用Channel和Flow的方法具有最少的样板代码。我们可以定义一个扩展函数来处理事件的发送和收集,从而在整个应用程序中实现一致性。
下面是一个示例代码片段,展示了如何使用Channel和Flow来处理一次性事件:
// 在 ViewModel 中定义一个 Channel
private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow: Flow<Event> get() = eventChannel.receiveAsFlow()
// 在需要发送事件的地方调用该函数
fun sendEvent(event: Event) {
viewModelScope.launch {
eventChannel.send(event)
}
}
// 在 UI 层收集事件
viewModel.eventsFlow.collect { event ->
// 处理事件
}
通过使用Channel和Flow,我们可以实现更灵活、可测试和可靠的一次性事件处理。我们可以根据需要在ViewModel和UI层之间进行事件通信,并确保事件的正确传递和消费。
选项3 — EventBus
虽然它有一百个好处,但是呢,由于小编之前接手的EventBus项目13,都不知道从哪里吃起,就不解释了
总结一下,
使用这些方法来处理Android中的一次性事件,只是众多方法中的冰山一角。正如前面所说每种方法都可能存在缺陷和优点, 灵活、可测试和可靠地使用这些方法,才能使我们能够更好地管理和处理应用程序中的事件流。
收录于合集 #kotlin基础
13个
上一篇Android中的多态响应处理