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

相关推荐
阿巴斯甜19 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker19 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952720 小时前
Andorid Google 登录接入文档
android
黄林晴21 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android