0Pricing
Kotlin Academy · 课时

使用 shareIn 与 stateIn 将冷 Flow 转换为热 Flow

使用共享运算符将冷 Flow 转换为热流。

使用 shareIn 与 stateIn 将冷 Flow 转换为热 Flow 是 CoddyKit 上的免费 Kotlin Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Kotlin Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Kotlin Academy 课程共包含 4 节课。

冷流与热流

冷流会为每个收集器重新启动;热流共享一个上游订阅。将冷流转换为热流,可以在多个收集器订阅时避免重复工作。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun expensiveFlow() = flow {
    println("Starting upstream work")  // runs once if shared
    repeat(3) { delay(100); emit(it) }
}
fun main() = runBlocking {
    val cold = expensiveFlow()
    // Two collectors = two executions:
    launch { cold.collect { } }
    launch { cold.collect { } }
    delay(500)
    coroutineContext.cancelChildren()
}

shareIn 基础

flow.shareIn(scope, started, replay) 会将冷 Flow 转换为 SharedFlow,并共享一个上游订阅。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val cold = flow {
        println("Upstream started once")
        repeat(5) { delay(100); emit(it) }
    }
    val hot = cold.shareIn(this, SharingStarted.Eagerly, replay = 0)
    launch { hot.collect { println("A: $it") } }
    launch { hot.collect { println("B: $it") } }
    delay(600)
    coroutineContext.cancelChildren()
}

stateIn 基础

flow.stateIn(scope, started, initialValue) 会将流转换为 StateFlow——它始终有一个值,并重放 1 个值。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val cold = flow { delay(100); emit(42) }
    val state: StateFlow<Int> = cold.stateIn(
        scope = this,
        started = SharingStarted.Eagerly,
        initialValue = 0
    )
    println(state.value)  // 0 immediately
    delay(200)
    println(state.value)  // 42 after upstream emits
    coroutineContext.cancelChildren()
}

SharingStarted.立即启动

立即启动:无论是否有订阅者,调用 shareIn/stateIn 时上游都会立即启动。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val flow = flow {
        println("Eagerly started")
        emit(1)
    }.shareIn(this, SharingStarted.Eagerly)
    // Upstream already running even before any collect
    delay(50)
    launch { flow.collect { println(it) } }
    delay(100)
    coroutineContext.cancelChildren()
}

SharingStarted.延迟启动

延迟启动:上游会在第一个订阅者订阅时启动,并且永不停止(即使订阅者数量降为零)。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val flow = flow {
        println("Lazily started on first subscriber")
        repeat(3) { delay(100); emit(it) }
    }.shareIn(this, SharingStarted.Lazily, replay = 1)
    delay(50)  // no subscriber yet — not started
    launch { flow.collect { println(it) } }  // triggers start
    delay(400)
    coroutineContext.cancelChildren()
}

SharingStarted.WhileSubscribed

WhileSubscribed(stopTimeout):第一个订阅者加入时启动上游,最后一个订阅者离开 stopTimeout 毫秒后停止。非常适合 ViewModels。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val flow = flow {
        println("Started")
        repeat(10) { delay(100); emit(it) }
    }.shareIn(this, SharingStarted.WhileSubscribed(500))
    val job = launch { flow.collect { print("$it ") } }
    delay(300)
    job.cancel()    // subscriber left
    delay(200)      // within 500ms stop timeout — still running
    launch { flow.collect { print("resume $it ") } }
    delay(500)
    coroutineContext.cancelChildren()
}

典型的 ViewModel 模式

在 Android ViewModels 中,使用 stateIn(viewModelScope, WhileSubscribed(5000), initialValue) 转换仓库流,以便共享流并在配置更改后继续保留。

import kotlinx.coroutines.flow.*
// In ViewModel:
// val uiState: StateFlow<UiState> = repository
//     .dataFlow()
//     .map { UiState.Success(it) }
//     .stateIn(
//         scope = viewModelScope,
//         started = SharingStarted.WhileSubscribed(5_000),
//         initialValue = UiState.Loading
//     )
fun main() { println("WhileSubscribed(5000) is the recommended ViewModel pattern") }

shareIn 与 stateIn

shareIn → SharedFlow(没有当前值,可配置重放)。stateIn → StateFlow(始终有一个值,重放值为 1)。请根据消费者是否需要当前状态进行选择。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val cold = (1..3).asFlow().map { it * 10 }

    // SharedFlow — no initial value:
    val shared: SharedFlow<Int> = cold.shareIn(this, SharingStarted.Eagerly, replay = 1)

    // StateFlow — always has a value:
    val state: StateFlow<Int> = cold.stateIn(this, SharingStarted.Eagerly, 0)

    delay(100)
    println("shared cache: ${shared.replayCache}")
    println("state value: ${state.value}")
    coroutineContext.cancelChildren()
}

重放缓存的权衡

重放值越大,较晚订阅的订阅者看到的历史越多,但占用的内存也越多。对于界面状态,重放值为 1(stateIn)就足够了。对于事件日志,可能需要更大的重放值。

import kotlinx.coroutines.flow.*
// replay=0: no history, only future events
// replay=1: last value (equivalent to stateIn)
// replay=N: last N events — use for message feeds, logs

// Trade-off: memory vs subscriber freshness
fun main() { println("Choose replay based on late-subscriber requirements") }

未共享流的风险

如果不进行共享,每个 Compose 收集器或 ViewModel 观察者都会重新运行上游流,从而产生重复的网络请求和数据库查询。请始终共享开销较大的流。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun expensiveApi() = flow {
    println("API CALL")  // without sharing: once per collector
    emit("data")
}
fun main() = runBlocking {
    val shared = expensiveApi().shareIn(this, SharingStarted.Lazily, replay = 1)
    launch { shared.collect { } }  // one API call, shared
    launch { shared.collect { } }  // same emission
    delay(200)
    coroutineContext.cancelChildren()
}

共享时的资源清理

共享流的作用域被取消时,上游流也会被取消,其 finally 代码块会执行,从而正确清理资源。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun resourceFlow() = flow {
    try { repeat(10) { delay(100); emit(it) } }
    finally { println("Upstream cleaned up") }
}
fun main() = runBlocking {
    val scope = CoroutineScope(SupervisorJob())
    val shared = resourceFlow().shareIn(scope, SharingStarted.Eagerly)
    launch { shared.collect { print("$it ") } }
    delay(250)
    scope.cancel()  // upstream cleanup runs
    delay(100)
}

快速检查

对于 Android ViewModels,推荐使用哪种 SharingStarted 策略?

回顾

shareIn 会转换为 SharedFlow;stateIn 会转换为 StateFlow。在 ViewModels 中使用 WhileSubscribed 可以节省资源。当多个收集器订阅时,共享可以避免重复执行上游流。

常见问题解答

「使用 shareIn 与 stateIn 将冷 Flow 转换为热 Flow」课时是免费的吗?

是的 — 「使用 shareIn 与 stateIn 将冷 Flow 转换为热 Flow」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Kotlin Academy 课程的其余内容,请升级到 CoddyKit PRO。 Kotlin Academy 课程共包含 4 节课。

「使用 shareIn 与 stateIn 将冷 Flow 转换为热 Flow」这节课中我会学到什么?

使用共享运算符将冷 Flow 转换为热流。 你通过在浏览器中直接运行的动手代码来练习 Kotlin Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Kotlin Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Kotlin Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「使用 shareIn 与 stateIn 将冷 Flow 转换为热 Flow」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Kotlin Academy 课中编写并运行代码吗?

能。每节 Kotlin Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. StateFlow:用于界面的热状态容器
  2. SharedFlow:事件总线与一次性事件
  3. 使用 shareIn 与 stateIn 将冷 Flow 转换为热 Flow
  4. 使用 Turbine 测试 StateFlow 与 SharedFlow
← 返回 Kotlin Academy