Kotlin Flow 使用


一、Flow:模拟分页网络请求

体现的特点

  • flow {} 创建的是冷流
  • 仅仅创建 Flow 对象,上游代码不会执行。
  • 只有调用 collectflow {} 中的代码才会执行。
  • 每调用一次 collect,都会重新执行一次上游代码。
  • 多个收集者拥有各自独立的数据生产过程。

完整代码

kotlin 复制代码
package com.example.flowdemo

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch

class ColdFlowActivity : ComponentActivity() {

    companion object {
        private const val TAG = "ColdFlowDemo"
    }

    /**
     * 使用 flow {} 创建一个冷流。
     *
     * 注意:
     * 执行到 val userFlow = flow { ... } 时,
     * 只是创建了一个 Flow 对象,并不会立即执行大括号中的代码。
     *
     * 只有后面调用 userFlow.collect 时,
     * flow {} 中的代码才会真正开始执行。
     */
    private val userFlow: Flow<String> = flow {

        Log.d(TAG, "开始执行网络请求")

        // 模拟依次请求三页数据
        for (page in 1..3) {

            // 模拟网络请求耗时
            // delay 只会挂起当前协程,不会阻塞线程
            delay(1000)

            val result = "第 $page 页用户数据"

            Log.d(TAG, "网络请求返回:$result")

            /**
             * emit 用于向下游发送一条数据。
             *
             * 下游 collect 中的代码会接收到该数据。
             */
            emit(result)
        }

        Log.d(TAG, "网络请求执行结束")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        /**
         * lifecycleScope.launch 启动一个与 Activity 生命周期绑定的协程。
         *
         * Activity 销毁时,这个协程会自动取消。
         */
        lifecycleScope.launch {

            /**
             * coroutineScope 会创建一个子协程作用域。
             *
             * 下面启动的收集者 A 和收集者 B,
             * 都属于这个 coroutineScope。
             */
            coroutineScope {

                /**
                 * 启动第一个收集者。
                 *
                 * 调用 collect 后,
                 * userFlow 中 flow {} 的代码会执行一次。
                 */
                launch {
                    userFlow.collect { data ->

                        // data 就是上游通过 emit(result) 发出的数据
                        Log.d(TAG, "收集者 A 收到:$data")
                    }
                }

                // 等待 500 毫秒,再启动第二个收集者
                delay(500)

                /**
                 * 启动第二个收集者。
                 *
                 * 因为普通 Flow 是冷流,
                 * 所以这里调用 collect 时,
                 * flow {} 中的代码会重新独立执行一次。
                 */
                launch {
                    userFlow.collect { data ->
                        Log.d(TAG, "收集者 B 收到:$data")
                    }
                }
            }
        }
    }
}

关键日志

text 复制代码
开始执行网络请求
开始执行网络请求

网络请求返回:第 1 页用户数据
收集者 A 收到:第 1 页用户数据

网络请求返回:第 1 页用户数据
收集者 B 收到:第 1 页用户数据

为什么"开始执行网络请求"会打印两次?

因为 Flow 是冷流。

虽然两个收集者使用的是同一个对象:

kotlin 复制代码
userFlow

但是每调用一次:

kotlin 复制代码
userFlow.collect { }

都会重新执行一次:

kotlin 复制代码
flow {
    Log.d(TAG, "开始执行网络请求")

    // 其他上游代码
}

因此:

kotlin 复制代码
// 收集者 A
userFlow.collect { }

会启动一套独立的数据生产过程。

kotlin 复制代码
// 收集者 B
userFlow.collect { }

又会启动另一套独立的数据生产过程。

两个收集者之间默认不会共享这次网络请求。

为什么收集者 B 从第一页开始接收?

因为收集者 B 并不是加入收集者 A 已经开始的那次请求。

当 B 调用 collect 时,会重新执行:

kotlin 复制代码
for (page in 1..3)

所以 B 也会从第一页开始生产数据。

大致执行过程如下:

text 复制代码
收集者 A 调用 collect
    ↓
执行一遍 flow {}
    ↓
发送第 1、2、3 页数据

收集者 B 调用 collect
    ↓
重新执行一遍 flow {}
    ↓
再次发送第 1、2、3 页数据

如果没有调用 collect 会怎样?

例如只执行:

kotlin 复制代码
val userFlow = flow {
    Log.d(TAG, "开始执行网络请求")
}

但没有执行:

kotlin 复制代码
userFlow.collect { }

那么日志不会打印,网络请求逻辑也不会执行。

因为创建冷流只是在描述:

当有人收集时,应该如何生产数据。

并没有立即开始生产数据。

面试总结

flow {} 创建的通常是冷流。只有调用 collect 时,上游代码才会开始执行;每个收集者都会独立执行一次上游逻辑,因此不同收集者之间默认不共享数据生产过程。


二、StateFlow:保存和更新页面状态

体现的特点

  • StateFlow 是热流。
  • 必须提供一个初始值。
  • 始终保存一个当前最新状态。
  • 可以通过 value 直接获取当前状态。
  • 新收集者开始收集时,会立即收到当前最新状态。
  • 新值与旧值通过 equals 比较后相等时,不会重复通知收集者。
  • 适合表示页面状态。

完整代码

kotlin 复制代码
package com.example.flowdemo

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

/**
 * 页面完整状态。
 *
 * UI 页面需要显示的状态,可以统一放在一个 data class 中。
 *
 * count:当前计数
 * message:页面提示文字
 */
data class CounterUiState(
    val count: Int = 0,
    val message: String = "页面初始化"
)

class StateFlowViewModel : ViewModel() {

    /**
     * MutableStateFlow 是可修改的 StateFlow。
     *
     * StateFlow 必须提供初始值。
     *
     * 当前初始状态为:
     * CounterUiState(
     *     count = 0,
     *     message = "页面初始化"
     * )
     *
     * 使用 private,避免 Activity 直接修改状态。
     */
    private val _uiState = MutableStateFlow(
        CounterUiState()
    )

    /**
     * 对外暴露只读的 StateFlow。
     *
     * Activity 可以:
     * 1. 读取 uiState.value
     * 2. collect 收集状态
     *
     * 但不能直接执行:
     * uiState.value = ...
     */
    val uiState: StateFlow<CounterUiState> =
        _uiState.asStateFlow()

    /**
     * 计数增加。
     */
    fun increaseCount() {

        /**
         * update 会读取当前旧状态 oldState,
         * 然后根据旧状态计算出一个新状态。
         *
         * update 是原子更新操作,
         * 比直接使用 _uiState.value 更适合并发修改场景。
         */
        _uiState.update { oldState ->

            /**
             * data class 的 copy 会复制原来的对象,
             * 然后只修改指定的属性。
             *
             * 例如旧状态是:
             * CounterUiState(
             *     count = 0,
             *     message = "页面初始化"
             * )
             *
             * 更新后变成:
             * CounterUiState(
             *     count = 1,
             *     message = "计数增加成功"
             * )
             */
            oldState.copy(
                count = oldState.count + 1,
                message = "计数增加成功"
            )
        }

        /**
         * update 内部最终会把新状态更新给 StateFlow。
         *
         * 可以简单理解为内部完成了类似:
         *
         * _uiState.value = 新状态
         *
         * 当新状态与旧状态不相等时,
         * 正在 collect 的收集者就会收到新状态。
         */
    }

    /**
     * 设置一个与当前状态完全相同的值。
     */
    fun setSameState() {

        /**
         * 右边的 _uiState.value 是当前状态。
         *
         * 再把当前状态赋值回去,
         * 相当于新值与旧值完全相同。
         */
        _uiState.value = _uiState.value

        /**
         * StateFlow 会使用 equals 比较新值和旧值。
         *
         * 当:
         *
         * newValue == oldValue
         *
         * 结果为 true 时,
         * StateFlow 认为状态没有发生有效变化,
         * 因此不会再次通知收集者。
         *
         * 所以下游 collect 不会再次执行,
         * 也不会产生新的日志。
         */
    }

    /**
     * 模拟页面状态不断更新。
     */
    fun startAutoCount() {

        /**
         * viewModelScope 是与 ViewModel 生命周期绑定的协程作用域。
         *
         * ViewModel 被清除时,
         * 其中启动的协程会自动取消。
         */
        viewModelScope.launch {

            delay(1000)

            // count:0 → 1
            increaseCount()

            delay(1000)

            // count:1 → 2
            increaseCount()

            delay(1000)

            /**
             * 再次设置相同状态。
             *
             * 因为新值与旧值相等,
             * 所以收集者不会再次收到数据。
             */
            setSameState()

            delay(1000)

            // count:2 → 3
            increaseCount()
        }
    }
}

class StateFlowActivity : ComponentActivity() {

    companion object {
        private const val TAG = "StateFlowDemo"
    }

    /**
     * 获取 StateFlowViewModel。
     *
     * 配置变化,例如屏幕旋转时,
     * ViewModel 通常不会立即销毁。
     */
    private val viewModel: StateFlowViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        /**
         * lifecycleScope 启动一个与 Activity 生命周期绑定的协程。
         */
        lifecycleScope.launch {

            /**
             * repeatOnLifecycle 的作用:
             *
             * 当 Activity 生命周期至少到达 STARTED 时,
             * 执行大括号中的收集逻辑。
             *
             * 当 Activity 低于 STARTED 状态时,
             * 取消大括号中的收集协程。
             *
             * Activity 再次回到 STARTED 时,
             * 会重新启动收集协程。
             */
            repeatOnLifecycle(Lifecycle.State.STARTED) {

                /**
                 * 开始收集 StateFlow。
                 *
                 * StateFlow 会立即把当前最新状态发送给新收集者。
                 */
                viewModel.uiState.collect { state ->

                    Log.d(
                        TAG,
                        "收到状态:count=${state.count},message=${state.message}"
                    )
                }
            }
        }

        // 开始模拟状态变化
        viewModel.startAutoCount()
    }
}

关键日志

text 复制代码
收到状态:count=0,message=页面初始化
收到状态:count=1,message=计数增加成功
收到状态:count=2,message=计数增加成功
收到状态:count=3,message=计数增加成功

count=2count=3 之间,没有产生重复的 count=2 日志。


为什么刚开始收集,就能收到 count=0

因为 StateFlow 始终保存一个当前值。

创建时已经提供了初始状态:

kotlin 复制代码
private val _uiState = MutableStateFlow(
    CounterUiState()
)

等价于:

kotlin 复制代码
private val _uiState = MutableStateFlow(
    CounterUiState(
        count = 0,
        message = "页面初始化"
    )
)

因此,在 Activity 开始收集之前,StateFlow 内部已经保存了这个状态。

当执行:

kotlin 复制代码
viewModel.uiState.collect { state ->
    Log.d(TAG, "$state")
}

StateFlow 会先立即将当前保存的最新状态交给新收集者。

所以会首先打印:

text 复制代码
收到状态:count=0,message=页面初始化

这就是 StateFlow 适合表示 UI 状态的重要原因:

UI 无论什么时候开始观察,都能够立即获得当前页面应该显示的状态。


为什么设置相同状态不会产生新日志?

代码是:

kotlin 复制代码
_uiState.value = _uiState.value

假设当前值为:

kotlin 复制代码
CounterUiState(
    count = 2,
    message = "计数增加成功"
)

那么这次赋值相当于:

kotlin 复制代码
旧值:
CounterUiState(
    count = 2,
    message = "计数增加成功"
)

新值:
CounterUiState(
    count = 2,
    message = "计数增加成功"
)

StateFlow 在发送数据之前,会通过 equals 判断新值和旧值是否相等。

可以简单理解为内部会进行类似判断:

kotlin 复制代码
if (newValue == oldValue) {
    // 状态没有发生有效变化
    // 不通知收集者
    return
}

因此:

kotlin 复制代码
_uiState.value = _uiState.value

新值和旧值相等,StateFlow 不会再次通知收集者,collect 中的代码也就不会执行。

所以不会产生新的日志。


必须是同一个对象,才不会重复发送吗?

不是。

StateFlow 判断的是:

kotlin 复制代码
newValue == oldValue

也就是调用 equals 比较,而不只是判断是否为同一个对象。

例如:

kotlin 复制代码
val state1 = CounterUiState(
    count = 2,
    message = "计数增加成功"
)

val state2 = CounterUiState(
    count = 2,
    message = "计数增加成功"
)

state1state2 是两个不同对象,但是因为 CounterUiStatedata class,会自动生成基于属性内容的 equals

所以:

kotlin 复制代码
state1 == state2

结果是:

text 复制代码
true

即使这样赋值:

kotlin 复制代码
_uiState.value = CounterUiState(
    count = 2,
    message = "计数增加成功"
)

只要当前状态的属性内容也是完全相同的,StateFlow 仍然不会重复通知收集者。


为什么 StateFlow 要过滤相同状态?

因为 StateFlow 表示的是当前状态

假设页面当前状态已经是:

kotlin 复制代码
CounterUiState(count = 2)

现在又设置:

kotlin 复制代码
CounterUiState(count = 2)

页面状态实际上没有变化。

如果还重复通知 UI,可能会导致没有必要的:

  • 文本重新赋值
  • Compose 重组
  • 列表刷新
  • UI 计算
  • 日志输出

因此 StateFlow 会过滤掉相等的状态,减少无意义的重复更新。

这也是 StateFlow 的状态合并特性


update 没有调用 emit,为什么也能发送状态?

代码是:

kotlin 复制代码
_uiState.update { oldState ->
    oldState.copy(
        count = oldState.count + 1
    )
}

updateMutableStateFlow 的扩展函数。

它内部会:

  1. 读取当前状态。
  2. 执行传入的转换逻辑。
  3. 得到新状态。
  4. 将新状态更新给 MutableStateFlow
  5. 如果新旧状态不相等,通知收集者。

所以可以简单理解为:

kotlin 复制代码
val oldState = _uiState.value

val newState = oldState.copy(
    count = oldState.count + 1
)

_uiState.value = newState

只不过 update 提供了原子更新能力,在多个协程同时修改状态时更加安全。


为什么 StateFlow 是热流?

因为 StateFlow 对象和它保存的状态,不依赖某个具体收集者而存在。

例如即使当前没有任何 Activity 收集:

kotlin 复制代码
_uiState.update {
    it.copy(count = 10)
}

StateFlow 的当前值仍然会被更新为:

kotlin 复制代码
CounterUiState(count = 10)

之后 Activity 才开始收集时,会立即收到最新的 count=10

它不像冷流那样需要通过 collect 才开始执行一次新的生产过程。


面试总结

StateFlow 是用于保存状态的热流,必须提供初始值,并始终保存一个当前最新值。新收集者开始收集时,会立即获得当前状态。StateFlow 会通过 equals 比较新旧值,如果新旧状态相等,就不会重复通知收集者,因此非常适合表示 UI 状态。


三、SharedFlow:发送一次性页面事件

体现的特点

  • SharedFlow 是热流。
  • 不要求必须提供初始值。
  • 可以将一条数据广播给多个正在收集的收集者。
  • 可以通过 replay 控制保存多少条历史数据。
  • replay = 0 时,不保存历史事件。
  • 新收集者不会收到订阅之前发送的事件。
  • 常用于 Toast、Snackbar、页面跳转等一次性事件。

完整代码

kotlin 复制代码
package com.example.flowdemo

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch

/**
 * 定义页面的一次性事件。
 *
 * sealed interface 可以限制事件类型的范围。
 *
 * 当前页面事件只有:
 * 1. 显示 Toast
 * 2. 跳转详情页
 */
sealed interface MainUiEvent {

    /**
     * 显示 Toast 事件。
     */
    data class ShowToast(
        val message: String
    ) : MainUiEvent

    /**
     * 跳转详情页事件。
     */
    data class NavigateToDetail(
        val userId: Long
    ) : MainUiEvent
}

class SharedFlowViewModel : ViewModel() {

    /**
     * 创建可发送数据的 SharedFlow。
     *
     * replay = 0 表示:
     * 不保存已经发送过的历史事件。
     *
     * 因此,新收集者只能收到订阅之后发送的事件。
     */
    private val _events = MutableSharedFlow<MainUiEvent>(
        replay = 0
    )

    /**
     * 对外暴露只读 SharedFlow。
     *
     * Activity 只能收集事件,
     * 不能直接发送事件。
     */
    val events: SharedFlow<MainUiEvent> =
        _events.asSharedFlow()

    /**
     * 在收集者注册之前发送事件。
     *
     * 这个方法主要用于演示 replay = 0 的效果。
     */
    fun sendEventBeforeCollect() {

        /**
         * tryEmit 会尝试立即发送事件。
         *
         * 它不是挂起函数,因此不需要放在协程中。
         *
         * 当前 SharedFlow 的 replay = 0,
         * 如果此时没有收集者,
         * 这个事件不会为未来的收集者保存。
         */
        _events.tryEmit(
            MainUiEvent.ShowToast("收集者注册之前的事件")
        )
    }

    /**
     * 发送显示 Toast 的事件。
     */
    fun showToast(message: String) {

        viewModelScope.launch {

            /**
             * emit 用于向 SharedFlow 发送事件。
             *
             * 所有当前正在收集 events 的收集者,
             * 都有机会收到这个事件。
             */
            _events.emit(
                MainUiEvent.ShowToast(message)
            )
        }
    }

    /**
     * 发送页面跳转事件。
     */
    fun navigateToDetail(userId: Long) {

        viewModelScope.launch {
            _events.emit(
                MainUiEvent.NavigateToDetail(userId)
            )
        }
    }
}

class SharedFlowActivity : ComponentActivity() {

    companion object {
        private const val TAG = "SharedFlowDemo"
    }

    private val viewModel: SharedFlowViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        /**
         * 此时 Activity 还没有开始 collect events。
         *
         * 因为 SharedFlow 设置了 replay = 0,
         * 所以这个事件不会保存下来。
         *
         * 后面注册的收集者 A 和 B 都不会收到它。
         */
        viewModel.sendEventBeforeCollect()

        /**
         * 收集者 A。
         */
        lifecycleScope.launch {

            repeatOnLifecycle(Lifecycle.State.STARTED) {

                viewModel.events.collect { event ->

                    /**
                     * 根据事件的具体类型执行不同操作。
                     */
                    when (event) {

                        is MainUiEvent.ShowToast -> {
                            Log.d(
                                TAG,
                                "收集者 A:显示 Toast:${event.message}"
                            )
                        }

                        is MainUiEvent.NavigateToDetail -> {
                            Log.d(
                                TAG,
                                "收集者 A:跳转详情页:${event.userId}"
                            )
                        }
                    }
                }
            }
        }

        /**
         * 收集者 B。
         *
         * 这里创建第二个收集者,
         * 用于体现 SharedFlow 的广播特点。
         */
        lifecycleScope.launch {

            repeatOnLifecycle(Lifecycle.State.STARTED) {

                viewModel.events.collect { event ->
                    Log.d(
                        TAG,
                        "收集者 B 也收到了事件:$event"
                    )
                }
            }
        }

        /**
         * 等待一段时间,
         * 确保收集者 A 和 B 已经开始收集。
         */
        lifecycleScope.launch {

            delay(1000)

            /**
             * 此时 A 和 B 都在收集。
             *
             * 所以两个收集者都会收到这个事件。
             */
            viewModel.showToast("保存成功")

            delay(1000)

            /**
             * 此时 A 和 B 仍然在收集。
             *
             * 所以两个收集者都会收到跳转事件。
             */
            viewModel.navigateToDetail(userId = 1001L)
        }
    }
}

关键日志

text 复制代码
收集者 A:显示 Toast:保存成功
收集者 B 也收到了事件:ShowToast(message=保存成功)

收集者 A:跳转详情页:1001
收集者 B 也收到了事件:NavigateToDetail(userId=1001)

不会出现:

text 复制代码
收集者注册之前的事件

为什么"收集者注册之前的事件"不会出现?

因为创建 SharedFlow 时设置了:

kotlin 复制代码
MutableSharedFlow<MainUiEvent>(
    replay = 0
)

replay 表示:

SharedFlow 要为未来的新收集者保留最近多少条历史数据。

当:

kotlin 复制代码
replay = 0

表示一条历史数据也不保存。

代码执行顺序是:

text 复制代码
发送"收集者注册之前的事件"
    ↓
此时没有收集者
    ↓
replay = 0,不保存历史事件
    ↓
事件被丢弃
    ↓
后面 A、B 才开始收集
    ↓
A、B 无法收到之前的事件

因此,不会产生对应日志。


如果把 replay 改成 1 会怎样?

修改为:

kotlin 复制代码
private val _events = MutableSharedFlow<MainUiEvent>(
    replay = 1
)

表示保存最近一条事件。

那么即使事件发送时还没有收集者:

kotlin 复制代码
_events.tryEmit(
    MainUiEvent.ShowToast("收集者注册之前的事件")
)

SharedFlow 也会把它保存到重放缓存中。

当收集者 A 和 B 后面开始收集时,它们会首先收到最近保存的一条事件。

但是对于 Toast、页面跳转这种一次性事件,通常使用:

kotlin 复制代码
replay = 0

否则新页面重新开始收集时,可能会再次执行旧事件。

例如:

text 复制代码
用户已经跳转过一次详情页
    ↓
屏幕旋转,Activity 重新收集
    ↓
SharedFlow 重放旧跳转事件
    ↓
页面可能再次跳转

为什么 A 和 B 都能收到事件?

因为 SharedFlow 会把发送的数据广播给所有当前正在收集它的收集者。

发送:

kotlin 复制代码
_events.emit(
    MainUiEvent.ShowToast("保存成功")
)

此时 A 和 B 都在收集:

kotlin 复制代码
viewModel.events.collect { }

所以执行过程是:

text 复制代码
ViewModel 发送 ShowToast 事件
                ↓
         SharedFlow 广播
          ↙             ↘
     收集者 A         收集者 B

因此会出现两条日志:

text 复制代码
收集者 A:显示 Toast:保存成功
收集者 B 也收到了事件:ShowToast(message=保存成功)

这和普通冷流不同。

冷流中的两个收集者会分别执行各自的上游代码,而 SharedFlow 的多个收集者接收的是同一次发送的数据。


为什么 SharedFlow 是热流?

因为 SharedFlow 实例独立于某一个具体收集者存在。

ViewModel 可以在任何时候执行:

kotlin 复制代码
_events.emit(event)

发送行为不是由某个 collect 创建出来的独立生产过程。

多个收集者只是订阅同一个已经存在的 SharedFlow。

但是要注意:

热流独立存在,不代表它一定会保存所有事件。

是否保存历史数据,要由 replay 和缓冲区配置决定。

当前代码:

kotlin 复制代码
replay = 0

不会保存历史事件。


emittryEmit 有什么区别?

emit

kotlin 复制代码
_events.emit(event)

是挂起函数,必须在协程或挂起函数中调用:

kotlin 复制代码
viewModelScope.launch {
    _events.emit(event)
}

当缓冲区没有空间或者收集者暂时处理不过来时,emit 可以挂起等待。

tryEmit

kotlin 复制代码
_events.tryEmit(event)

不是挂起函数,可以直接调用。

它会立即尝试发送,并返回一个 Boolean:

kotlin 复制代码
val success = _events.tryEmit(event)

但"成功"不一定表示某个收集者实际接收到了数据。

例如当前:

kotlin 复制代码
replay = 0

并且没有任何收集者时,tryEmit 可以立即完成,但事件不会为未来收集者保存。


SharedFlow 用于一次性事件时的注意点

使用:

kotlin 复制代码
MutableSharedFlow<MainUiEvent>(
    replay = 0
)

发送一次性事件时,如果页面此时没有处于收集状态,事件可能丢失。

例如:

text 复制代码
Activity 进入后台
    ↓
repeatOnLifecycle 停止收集
    ↓
ViewModel 发送跳转事件
    ↓
replay = 0,不保存事件
    ↓
Activity 回到前台
    ↓
无法收到之前的事件

因此具体项目中要根据业务语义选择:

  • 事件只对当前活跃页面有效:可以使用 SharedFlow(replay = 0)
  • 事件必须确保被处理:可以考虑 Channel
  • 需要新收集者收到最近数据:配置合适的 replay
  • 表示页面当前状态:应使用 StateFlow,而不是 SharedFlow。

面试总结

SharedFlow 是可以向多个收集者广播数据的热流。它不要求初始值,并且可以通过 replay 控制新收集者能够收到多少条历史数据。replay = 0 时不保存历史事件,因此常用于 Toast、Snackbar、页面跳转等一次性事件。


四、三个示例的核心区别

对比项 flow {} StateFlow SharedFlow
默认类型 冷流 热流 热流
是否依赖收集者开始生产
是否必须有初始值
是否保存当前值 始终保存一个 根据 replay 决定
新收集者收到什么 重新执行完整上游 当前最新状态 最近 replay 条数据
新旧值相等时是否重复发送 上游决定 不会重复发送 会发送,每次事件彼此独立
多收集者是否共享数据 默认不共享 共享 共享
典型用途 网络请求、数据库查询、异步数据处理 UI 页面状态 一次性事件、广播消息

五、最终复习版

text 复制代码
Flow:

普通 flow {} 是冷流。
创建 Flow 对象时不会执行上游代码,只有调用 collect 才会执行。
每个收集者都会重新执行一次 flow {},默认不共享数据生产过程。
text 复制代码
StateFlow:

StateFlow 是用于保存状态的热流,必须提供初始值。
它始终保存一个当前最新状态,新收集者会立即收到当前状态。
StateFlow 使用 equals 比较新旧状态,新旧值相等时不会重复通知收集者。
适合保存 UI 状态。
text 复制代码
SharedFlow:

SharedFlow 是用于广播数据的热流,可以同时通知多个收集者。
它不要求初始值,并通过 replay 决定是否保存历史数据。
replay = 0 时,新收集者收不到订阅之前发送的事件。
适合 Toast、Snackbar、页面跳转等一次性事件。

最核心的一句话是:

text 复制代码
Flow:每次 collect 都重新生产数据。

StateFlow:保存一个当前状态,相同状态不重复通知。

SharedFlow:向多个收集者广播事件,是否保留历史事件由 replay 决定。