StateFlow 与 SharedFlow:在协程中管理状态与事件

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=1SharedFlow,只缓存最新的一个值,其 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

相关推荐
Lei活在当下7 小时前
【项目踩坑实录】并发环境下,Glide缓存引起的图片加载异常
android·debug·glide
my_power5209 小时前
检出git项目到android studio该如何配置
android·git·android studio
三少爷的鞋12 小时前
Repository 方法设计:suspend 与 Flow 的决选择指南(以朋友圈为例)
android
阿里云云原生13 小时前
Android App 崩溃排查指南:阿里云 RUM 如何让你快速从告警到定位根因?
android·java
ULTRA??14 小时前
归并排序算法实现,kotlin,c++,python
c++·python·kotlin
cmdch201715 小时前
手持机安卓新增推送按钮功能
android
攻城狮201515 小时前
【rk3528/rk3518 android14 kernel-6.10 emcp sdk】
android
何妨呀~15 小时前
mysql 8服务器实验
android·mysql·adb
QuantumLeap丶16 小时前
《Flutter全栈开发实战指南:从零到高级》- 25 -性能优化
android·flutter·ios
木易 士心17 小时前
MVC、MVP 与 MVVM:Android 架构演进之路
android·架构·mvc