Flow 是一个 "冷" 流,它只提供了数据流的生产逻辑。在被收集时才会开始生产,并且每次收集都会触发一套全新的、独立的生产流程。
对于事件订阅的复杂场景,比如用户点击、显示一个 Snackbar,这些事件不应该给每个新的订阅者都重发一遍。为此,协程提供了特殊的 "热" 流:SharedFlow。
SharedFlow:事件流订阅
我们调用 .shareIn 即可将一个冷流转换成一个可共享的热流,从数据收集转变为事件订阅。它会在内部使用指定的协程作用域来启动一个协程,并在其中收集并转发冷流的数据。
和 Channel 类似,数据生产和收集流程是独立的,但和 Channel 不同的是,它生产出的单条数据会发给每一个订阅者。
kotlin
fun main(): Unit = runBlocking {
val flow: Flow<Int> = flow {
for (num in 1..5) {
delay(100)
emit(num)
}
}
val sharedFlow: SharedFlow<Int> = flow.shareIn(
scope = this,
started = SharingStarted.Eagerly // 启动策略为立即启动
)
launch {
delay(150)
sharedFlow.collect {
println("Subscriber 1 collected $it")
}
}
launch {
delay(250)
sharedFlow.collect {
println("Subscriber 2 collected $it")
}
}
}
运行结果:
less
Subscriber 1 collected 2
Subscriber 1 collected 3
Subscriber 2 collected 3
Subscriber 1 collected 4
Subscriber 2 collected 4
Subscriber 1 collected 5
Subscriber 2 collected 5
注意:程序并没有结束,待会会讲到原因。
可以看到 "Subscriber 1" 并不能收到在它订阅之前发送的数据 1(不能收到完整的数据序列)。
订阅者会错过在它开始订阅之前已经广播过的数据,这行为并没有问题,因为对于事件订阅来说,我们通常只关心订阅之后发生的新事件。
我们来看一个 channelFlow 的例子:
kotlin
fun main(): Unit = runBlocking {
val channelFlow = channelFlow {
val callback: (Int) -> Unit = {
trySend(it)
}
Ticker.subscribe(callback)
awaitClose {
println("ChannelFlow closed, unsubscribe callback.")
Ticker.unsubscribe(callback)
}
}
Ticker.start()
launch {
delay(1500)
channelFlow.collect {
println("Subscribe-1 time: $it")
}
}
launch {
delay(3500)
channelFlow.collect {
println("Subscribe-2 time: $it")
}
}
delay(5500)
cancel()
}
/**
* 定时器
*/
object Ticker {
private var time = 0
set(value) {
field = value
// 触发所有订阅回调
subscribers.forEach {
it.invoke(time)
}
}
private val subscribers = mutableListOf<(Int) -> Unit>()
/**
* 订阅
*/
fun subscribe(callback: (Int) -> Unit) {
subscribers.add(callback)
}
/**
* 取消订阅
*/
fun unsubscribe(callback: (Int) -> Unit) {
subscribers.remove(callback)
}
@OptIn(DelicateCoroutinesApi::class)
fun start() {
GlobalScope.launch {
while (isActive) {
delay(1000)
time++
}
}
}
}
运行结果:
less
Subscribe-1 time: 2
Subscribe-1 time: 3
Subscribe-1 time: 4
Subscribe-2 time: 4
Subscribe-1 time: 5
Subscribe-2 time: 5
ChannelFlow closed, unsubscribe callback.
ChannelFlow closed, unsubscribe callback.
总结:可以看到,channelFlow 本身是"冷"流,但由于它订阅的是一个共享的外部热源,因此它最终也产生了"热"的效果。
shareIn 操作符
kotlin
public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>
shareIn 操作符有三个参数,其中 scope 是用来指定上游 Flow 所在的协程作用域。
replay:指定对于新订阅者重播最近数据的个数。
kotlin
fun main(): Unit = runBlocking {
val coldFlow = flow {
(1..3).forEach {
delay(100)
emit(it)
}
}
val scope = CoroutineScope(currentCoroutineContext())
val sharedFlow = coldFlow.shareIn(
scope = scope,
started = SharingStarted.Eagerly,
replay = 2
)
scope.launch {
sharedFlow.collect {
println("Subscriber 1: $it")
}
}
scope.launch {
delay(500)
sharedFlow.collect {
println("Subscriber 2: $it")
}
}
}
replay = 2 表示新订阅者会立即收到最近的 2 个历史数据。
运行结果:
kotlin
Subscriber 1: 1
Subscriber 1: 2
Subscriber 1: 3
Subscriber 2: 2
Subscriber 2: 3
即使 "Subscriber 2" 错过了所有数据的广播时间,但它还是收到了两条被缓存起来的数据。
replay 除了为新订阅者提供了缓存之外,还为当前活跃的订阅者提供了缓冲区。
started:上游 Flow 启动策略。
-
SharingStarted.Eagerly是立即启动 -
SharingStarted.Lazily是在SharedFlow第一次被订阅时启动。 -
SharingStarted.WhileSubscribed()是一种可以结束和重启上游的策略。它会在
SharedFlow第一次被订阅时启动,并且在所有订阅者取消订阅后,会结束上游的生产流程(节省资源)。 如果在这之后,有新的订阅者,会重启上游的数据流。
不过,我们先要知道 SharedFlow 的订阅是不会自动结束的,也就是 collect 并不会随着上游的生产结束而结束,只要内部不抛出异常,会一直运行不返回。
kotlin
// SharedFlow.kt
public interface SharedFlow<out T> : Flow<T> {
//...
override suspend fun collect(collector: FlowCollector<T>): Nothing // Nothing 表示永远不会返回
}
了解了这个,我们再来看代码,就能明白为什么要手动取消第一个订阅所在的协程。
kotlin
fun main(): Unit = runBlocking {
val coldFlow = flow {
(1..3).forEach {
delay(100)
emit(it)
}
}
val scope = CoroutineScope(currentCoroutineContext())
val sharedFlow = coldFlow.shareIn(
scope = scope,
started = SharingStarted.WhileSubscribed(),
replay = 1
)
val job = scope.launch {
sharedFlow.collect {
println("Subscriber 1: $it")
}
}
scope.launch {
delay(1000)
job.cancel() // 关键:订阅者1取消订阅
job.join() // 等待订阅者1完全结束
sharedFlow.collect {
println("Subscriber 2: $it")
}
}
}
这是为了让第一个 collect()(订阅)能够被取消。
运行结果:
kotlin
Subscriber 1: 1
Subscriber 1: 2
Subscriber 1: 3
Subscriber 2: 3 // 重启之前的缓存值
Subscriber 2: 1
Subscriber 2: 2
Subscriber 2: 3
可以看到,虽然上游数据流重启了,缓存并不会被丢弃。
另外,这个 WhileSubscribed() 有两个参数,分别可以设置没有订阅者后的结束宽限期 和缓存失效时间。
kotlin
@Suppress("FunctionName")
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
): SharingStarted
创建 MutableSharedFlow
对于事件流的订阅,常常需要在任何地方来手动发送事件(如点击的回调中)。这时,需要使用 MutableSharedFlow()。
MutableSharedFlow 实现了 FlowCollector,所以我们可以从外部调用 .emit() 函数来发送数据。
kotlin
// --- 在 ViewModel 中 ---
// 创建一个可变的 SharedFlow
private val _events = MutableSharedFlow<String>(replay = 0)
// 暴露只读的 SharedFlow 给外部订阅
val events: SharedFlow<String> = _events.asSharedFlow()
// 在任何地方发送事件
fun onLoginClicked() {
// 假设这里有一个 ViewModelScope
viewModelScope.launch {
_events.emit("导入按钮被点击") // 从 SharedFlow 外部发送数据
}
}
// --- 在 Activity/Fragment 的协程作用域中 ---
// 在 UI 层订阅事件
viewModel.events.collect { event ->
println("打开系统文件管理器")
}
当你需要凭空创建一个事件流时,就调用 MutableSharedFlow()。如果已经存在一个生产事件流的 Flow,就调用 shareIn 操作符将其转换成 SharedFlow。
我们来看看 MutableSharedFlow() 的参数:
replay:对于新订阅者的缓存大小extraBufferCapacity:额外的缓冲大小,比如replay=3、extraBufferCapacity=5的缓冲大小是 8。onBufferOverflow:缓冲溢出时的策略。
replay 的缓存溢出策略 永远是丢弃最旧数据(DROP_OLDEST)。
StateFlow:特殊的 SharedFlow
对于状态场景,比如当前的待办事项列表、当前的排序规则,这种始终存在的值,我们不仅要能在它变换时收到通知,还希望能随时读取它的当前快照。
这时我们可以使用 StateFlow,它是 SharedFlow 的子接口,专注于状态的订阅。
kotlin
public interface StateFlow<out T> : SharedFlow<T> {
public val value: T
}
你可以把它看作是一个 replay=1 的 SharedFlow,只缓存最新的一个值,其 value 属性就是 SharedFlow 中最新的事件值。
kotlin
// --- 在 ViewModel 中 ---
// 创建一个私有的、可变的 MutableStateFlow
private val _uiState = MutableStateFlow(0) // 必须提供初始值
// 封装一个公有的、只读的 StateFlow
val uiState = _uiState.asStateFlow()
// 更新状态
fun updateState(newValue: Int) {
_uiState.value = newValue
}
// --- 在 Activity/Fragment 的协程作用域中 ---
// 在 UI 层订阅状态
viewModel.uiState.collect {
println(it)
}
stateIn() 操作符
我们可以调用 stateIn() 将一个冷流转换成 StateFlow。