Android中的一次性事件

在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. 1. 事件被发送到流中。

  2. 2. 在Compose层的收集器接收它并对其进行处理。

  3. 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. 1. 即使在事件被发送时没有收集器存在,它也可以工作。事件将保留在流中,直到收集器返回到STARTED状态以消费它。

  2. 2. 纯粹的Flow,无需引入LiveData的依赖。

  3. 3. 最少的样板代码。

  4. 4. 易于单元测试和推理

  5. 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中的多态响应处理

转自:​Android中的一次性事件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值