文章

状态和事件

状态和事件

” 状态 “ 与 “ 事件 “

虽然 “ 状态 “ 和 “ 事件 “ 都可以通过响应式的方式通知到 UI 侧,但是它们的消费场景不同:

  • 状态(State):是需要 UI 长久呈现的内容,在新的状态到来之前呈现的内容保持不变。比如显示一个 Loading 框或是显示一组请求的数据集。状态具有粘性
  • 事件(Event):是需要 UI 即时执行的动作,是一个短期行为。比如显示一个 Toast、SnackBar,或者完成一次页面导航等。

基于 LiveData 的事件处理

见 [[LiveData#LiveData黏性和数据倒灌(LiveData为何会收到Observe之前的消息?)]]

基于 SharedFlow 的事件处理

StateFlow 和 LiveData 一样具备 “ 粘性 “ 特性,同样有 “ 数据倒灌 “ 的问题,甚至更有过之还会出现 “ 数据丢失 “ 的问题,因为 StateFlow 进行 updateState 时会过滤对新旧数据进行比较,同样类型的事件有可能被丢弃。

Roman Elizarov 曾在 Shared flows, broadcast channels. See how shared flows made broadcast… 一文中提出用 SharedFlow 实现 EventBus 的做法:

1
2
3
4
5
6
7
8
class BroadcastEventBus {
    private val _events = MutableSharedFlow<Event>()
    val events = _events.asSharedFlow() // read-only public view

    suspend fun postEvent(event: Event) {
        _events.emit(event) 
    }
}

它可以有多个收集器(订阅者),多个收集器 “ 共享 “ 事件,实现事件的广播,如下图所示:

kei90

  • 其次,SharedFlow 的数据会以流的形式发送,不会丢失,新事件不会覆盖旧事件;
  • 它的数据不是粘性的,消费一次就不会再次出现。

但是,SharedFlow 存在一个问题,接收器无法接收到 collect 之前发送的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MainViewModel : ViewModel(), DefaultLifecycleObserver {

    private val _toast = MutableSharedFlow<String>()
    val showToast = _toast.asSharedFlow()
    
    init {
        viewModelScope.launch {
            delay(1000)
            _toast.emit("Toast")
        }
    }
}

//Fragment side
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        mainViewModel.showToast.collect {
            Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
        }
    }
}

我们使用 repeatOnLifecycle 保证了事件收集在 STARTD 之后开始,如果此时注释掉 delay(1000) 的代码,emit 早于 collect,所以 toast 将无法显示。

有些时候我们在订阅出现之前就发出事件,并希望订阅者出现时执行响应这个事件,比如完成一个初始化任务等,注意这并非一种 “ 数据倒灌 “,因为这它只被允许消费一次,一旦消费就不再发送,所以 SharedFlow 的 replay 参数不能使用,因为 repaly 不能保证只消费一次。

基于 Channel 的处理事件

针对 SharedFlow 的这个不足, Roman Elizarov 也给了解决方案,即使用 Channel。

1
2
3
4
5
6
7
8
class SingleShotEventBus {
    private val _events = Channel<Event>()
    val events = _events.receiveAsFlow() // expose as flow

    suspend fun postEvent(event: Event) {
        _events.send(event) // suspends on buffer overflow
    }
}

当 Channel 没有订阅者时,向其发送的数据会挂起,保证订阅者出现时第一时间接收到这个数据,类似于阻塞队列的原理。 Channel 本身也是 Flow 实现的基础,所以通过 receiveAsFlow 可以转成一个 Flow 暴露给订阅者。

改造下前面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainViewModel : ViewModel(), DefaultLifecycleObserver {

    private val _toast = Channel<String>()
    val showToast = _toast.receiveAsFlow()
    
    init {
        viewModelScope.launch {
            _toast.send("Toast")
        }
    }
}

//Fragment side
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        mainViewModel.showToast.collect {
            Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
        }
    }
}

UI 侧仍然针对 Flow 订阅,代码不做任何改动,但是在 STATED 之后也可以接受到已发送的事件。

需要注意,Channel 也有一个使用上的限制,当 Channel 有多个收集器时,它们不能共享 Channel 传输的数据,每个数据只能被一个收集器独享,因此 Channel 更适合一对一的通信场景。

a20je

综上,SharedFlow 和 Channel 在事件处理上各有特点,大家需要根据实际场景灵活选择:

 SharedFlowChannel
订阅者数量订阅者共享通知,可以实现一对多的广播每个消息只有一个订阅者可以收到,用于一对一的通信
事件接受collect 之前的事件会丢失第一个订阅者可以收到 collect 之前的事件
本文由作者按照 CC BY 4.0 进行授权