【Flow进阶篇三】SharedFlow 与 StateFlow 的详细对比及适用场景

1,138 阅读6分钟

前言

在 Kotlin中,StateFlowSharedFlow 常被用来处理应用中的数据流和状态,它们都是基于 Flow 的热流(hot stream)实现,虽然名字相似,但其实设计理念和使用场景有显著区别。本文将深入探讨这两者的特点、源码实现,并通过实际的应用场景帮助你选择最合适的工具。

一、什么是 SharedFlow?

SharedFlow 是 Kotlin 提供的一种流类型,用于处理“事件流”。首先,我们得理解什么是事件流。简单来说,事件流就是应用中发生的一个个事件,如用户点击按钮、网络请求完成等。SharedFlow 的主要作用是将这些事件传递给多个订阅者,确保所有活跃的订阅者能收到这些事件。

与普通的 Flow 不同,SharedFlow 是一个热流,它不会等待订阅者的到来,而是即时广播事件。如果某个订阅者在事件发布之前没有订阅到流,它将错过这个事件。为了避免这个问题,SharedFlow 提供了 replay 缓存机制,允许我们指定缓存多少个事件供新订阅者接收。

fun main() = runBlocking {
    val sharedFlow = MutableSharedFlow<String>(replay = 1)

        //事件触发
        launch {
            sharedFlow.emit("Event Triggered")
        }

        // 事件订阅
        sharedFlow.collect { value ->
            println("Received: $value")
        }
}

在这个例子中,MutableSharedFlow 会将事件 "Event Triggered" 发射给所有活跃订阅者。replay = 1 的设置意味着最新的一个事件会被缓存,并传递给新的订阅者。如果没有设置 replay,订阅者将错过事件。

SharedFlow 适合那些需要广播事件的场景,例如通知系统或多订阅者的消息推送。

二、什么是 StateFlow?

StateFlow 继承自 SharedFlow,但它的设计目标有所不同。StateFlow 用于持久化和管理“状态”,它可以确保每个订阅者始终能够接收到当前的状态值。与 SharedFlow 的“事件广播”不同,StateFlow 强调的是“状态的一致性”。

举个例子,当你管理一个 UI 状态时,StateFlow 确保所有订阅者都能获得当前的 UI 状态,而无论它们何时订阅。它是状态的“快照”,并且状态值会随着新的更新而改变。

fun main() = runBlocking {
    val stateFlow = MutableStateFlow("Initial State")

    // 模拟状态更新
    launch {
        stateFlow.value = "Updated State"
    }

    // 模拟订阅
    stateFlow.collect { value ->
        println("Current state: $value")
    }
}

在这个例子中,MutableStateFlow 用来保存一个字符串状态。当状态值更新时,所有的订阅者都会收到最新的状态值。与 SharedFlow 的事件广播不同,StateFlow 的目的是确保每个订阅者获取最新的状态。

StateFlow 适用于需要持久化状态的场景,例如 UI 状态管理。这个就类似Livedata,所以以后不要再去魔改Livedata做事件处理,因为他的设计上就是为了状态管理的。

三、StateFlow 源码剖析

StateFlow 本质上是一个特殊的 SharedFlow,它不仅会广播事件,还会维护一个当前状态。通过使用 MutableStateFlow,我们能够直接访问和修改状态值,而 StateFlow 确保所有订阅者都能获取到当前的状态。

对于 SharedFlowStateFlow 的具体实现,你可以查看我之前写的一篇关于 StateFlow 源码的文章,其中详细分析了 StateFlow 继承自 SharedFlow 的原因,并解释了它如何通过持久化当前状态来增强其功能。这样,开发者可以更好地理解它的设计原理。

image.png

# Kotlin StateFlow 源码解析

四、SharedFlow 与 StateFlow 的对比

通过上面的介绍,我们已经大致了解了这两者的不同。那么,它们到底有什么区别呢?下面我们通过对比的方式更清晰地阐明两者的优缺点。

特性SharedFlowStateFlow
主要功能事件广播状态管理,持久化当前状态
是否保存历史不保存历史,除非设置 replay始终保存最新状态
订阅者行为订阅者错过事件,除非设置 replay订阅者始终获取当前状态值
适用场景广播事件,多消费者共享事件状态管理,保持最新状态的一致性
1. 历史数据与当前状态

SharedFlow 是一个事件广播流,它传递的是发生的事件。例如,当用户点击按钮时,我们只关心点击事件本身,广播给所有订阅者。如果某个订阅者错过了这个事件,它就无法接收到。StateFlow 则是用来管理状态的,每个订阅者始终可以获取当前的状态。

2. 事件 vs 状态

SharedFlow 更适合用于事件广播,例如发送通知、按钮点击、网络请求完成等;它并不关心状态的持久化,只需要将事件广播给所有订阅者。

StateFlow 更适合用于状态管理,例如用户登录状态、UI 状态等。它保证每个订阅者都会接收到最新的状态值,因此特别适合管理全局或单一的状态。

五、如何根据场景选择 SharedFlow 或 StateFlow?

了解了它们的差异后,接下来我们可以根据实际的应用场景来选择合适的流类型。

1. 事件广播:SharedFlow 的应用

如果你的应用需要广播一些事件(例如用户的操作),SharedFlow 是最佳选择。在一个聊天室应用中,新加入的用户通常需要看到最近的几条消息,而不是从空白状态开始。这时,我们可以使用 SharedFlowreplay 机制,让新用户在加入时能够看到最新的几条消息。

fun main() = runBlocking {
    val chatFlow = MutableSharedFlow<String>(replay = 2) // 只缓存最近 2 条消息

    // 模拟聊天室中有用户在发送消息
    launch {
        chatFlow.emit("User1: Hello!")
        delay(100)
        chatFlow.emit("User2: Hi!")
        delay(100)
        chatFlow.emit("User3: Welcome!")
        delay(100)
        chatFlow.emit("User4: Nice to meet you!")
    }

    launch {
        delay(350) // 模拟新用户加入
        chatFlow.collect { message ->
            println("New user sees: $message")
        }
    }
    
    delay(10000)
}

在这个例子中,SharedFlow 被用来广播消息。如果某个用户在消息发出前没有订阅,便会错过该消息。但是可以用 replay 机制来回放历史消息。

示例输出(可能的结果):

New user sees: User3: Welcome!
New user sees: User4: Nice to meet you!
2. UI 状态管理:StateFlow 的应用

对于 UI 状态管理,StateFlow 是更合适的选择。例如,当你处理登录状态或其他用户相关的状态时,StateFlow 可以保证每个订阅者都始终获取到最新的状态。

class LoginViewModel : ViewModel() {
    private val _loginState = MutableStateFlow(LoginState.Idle)
    val loginState: StateFlow<LoginState> = _loginState

    fun login(username: String, password: String) {
        _loginState.value = LoginState.Loading
        viewModelScope.launch {
            delay(2000)
            _loginState.value = LoginState.Success("Login successful")
        }
    }
}

sealed class LoginState {
    object Idle : LoginState()
    object Loading : LoginState()
    data class Success(val message: String) : LoginState()
    data class Error(val error: String) : LoginState()
}

在这个例子中,StateFlow 确保 UI 始终展示最新的登录状态,保证用户体验的一致性。

3. SharedFlow 在日志系统中的缓冲区应用

假设我们有一个日志系统,后台的日志收集服务(消费者)处理日志的速度较慢,但前端服务(生产者)不断产生日志。如果日志服务跟不上处理速度,我们不希望日志立即丢失,而是希望能有一定的缓冲区。

object Logger {
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    private val logFlow = MutableSharedFlow<String>(
        replay = 0, // 不存储历史日志
        extraBufferCapacity = 100, // 允许 100 条日志缓存
        onBufferOverflow = BufferOverflow.DROP_OLDEST // 避免 emit 挂起,保证新日志可写入
    )

    init {
        // 启动日志写入任务
        scope.launch {
            logFlow.collect { log ->
                writeLogToFile(log)
            }
        }
    }

    fun log(message: String) {
        scope.launch {
            logFlow.emit(message) // 通过异步方式 emit,避免阻塞业务方
        }
    }

    private fun writeLogToFile(log: String) {
        val file = File("app_logs.txt")
        file.appendText("$log\n")
        println("Log written: $log") // 模拟写入日志
    }
}

fun main() = runBlocking {
    repeat(200) {
        Logger.log("Log message #$it") // 业务方调用日志
        delay(10) // 模拟业务运行
    }
}

打开app_logs.txt这个日志文件可以看到,日志完整的,而且也不会阻塞写日志的调用方。

总结

  • SharedFlow

    • 功能: 事件广播

    • 适用场景: 多订阅者共享事件,如消息推送、通知系统

    • 特点:

      • 不保存历史数据,除非设置 replay 缓存机制
      • 可设置 extraBufferCapacity 解决生产者和消费者速度不一致
      • onBufferOverflow 策略控制溢出行为
    • 缺点:

      • 订阅者错过事件,除非设置 replay
      • 排队机制和溢出策略配置不当可能影响性能
  • StateFlow

    • 功能: 状态管理

    • 适用场景: UI 状态管理、全局状态管理

    • 特点:

      • 始终保存最新状态,确保每个订阅者获得当前状态
    • 缺点: 不适合广播事件

  • 主要区别

    特性SharedFlowStateFlow
    功能事件广播状态管理
    历史数据不保存历史,除非 replay始终保存最新状态
    订阅者行为错过事件,除非 replay始终接收最新状态
    应用场景事件广播状态管理
  • 处理上下游速度不一致

    • SharedFlow 提供 extraBufferCapacityonBufferOverflow 策略来缓解生产者和消费者速度不一致的问题:

      • extraBufferCapacity: 增加缓存容量,允许更多数据积压
      • onBufferOverflow: 选择溢出策略(如 DROP_OLDESTSUSPEND)来避免生产者阻塞
    • 这种机制非常适用于日志系统或任何需要在速度差异较大的生产者和消费者之间传递数据的场景。

结论

根据需求选择合适的流类型:

  • SharedFlow 适合事件广播,并提供强大的缓冲和溢出处理机制。
  • StateFlow 适合管理 UI 状态或全局状态,保证状态的一致性。