状态和事件
” 状态 “ 与 “ 事件 “
虽然 “ 状态 “ 和 “ 事件 “ 都可以通过响应式的方式通知到 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)
}
}
它可以有多个收集器(订阅者),多个收集器 “ 共享 “ 事件,实现事件的广播,如下图所示:
- 其次,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 更适合一对一的通信场景。
综上,SharedFlow 和 Channel 在事件处理上各有特点,大家需要根据实际场景灵活选择:
SharedFlow | Channel | |
---|---|---|
订阅者数量 | 订阅者共享通知,可以实现一对多的广播 | 每个消息只有一个订阅者可以收到,用于一对一的通信 |
事件接受 | collect 之前的事件会丢失 | 第一个订阅者可以收到 collect 之前的事件 |