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

相关推荐
2501_9159090620 小时前
iOS APP 抓包全流程解析,HTTPS 调试、网络协议分析与多工具组合方案
android·ios·小程序·https·uni-app·iphone·webview
Propeller20 小时前
【Android】快速上手 Android 组件化开发
android·架构
那我掉的头发算什么21 小时前
【javaEE】多线程进阶--CAS与原子类
android·java·jvm·java-ee·intellij-idea
Yue丶越21 小时前
【Python】基础语法入门(二)
android·开发语言·python
q***087421 小时前
MySQL压缩版安装详细图解
android·mysql·adb
九鼎创展科技21 小时前
九鼎创展发布X3588SCV4核心板,集成LPDDR5内存,提升RK3588S平台性能边界
android·人工智能·嵌入式硬件·硬件工程
与籍同行1 天前
安卓10.0 分屏相关
android
BD_Marathon1 天前
Eclipse 代码自动补全设置
android·java·eclipse
w***74401 天前
SQL Server 数据库迁移到 MySQL 的完整指南
android·数据库·mysql
zgyhc20501 天前
【Android Audio】dumpsys media.metrics分析
android