SharedFlow与StateFlow的效果和适用场景

373 阅读14分钟

SharedFlow 是一种特殊的Flow,而StateFlow是一种特殊的SharedFlow,Flow虽然是一个数据流,但它只是设定好了数据流的规则,而并不是直接开始启动数据流声场流程,生产过程是在调用collect函数之后开始的,而且是每次调用collect都会启动一个新的数据流。虽然叫做数据流,但是在flow对象创建完成后,并没有开始流动,而是在收集的时候才开始流动,这也是为什么flow接收数据的函数名字叫做collect,而不是subscribe,因为它确实不是事件流的订阅,而是数据流的收集。Flow之所以这样设计,是因为collect的使用更加广泛,事件订阅其实就是一种特殊的数据流的收集。我们利用数据收集功能能够实现事件订阅的效果,而这种事件订阅的API已经有了,就是ShareFlow。

launchInshareIn

在了解ShareFlow之前,我们需要先了解launchInshareInlaunchInshareIn 是 Kotlin 中协程(Coroutines)库的一部分,主要用于协程的上下文启动和共享操作。

launchIn

launchIn 是一个用于启动流(Flow)的终端操作符,它将 Flow 的收集工作启动在给定的协程作用域中,并返回一个 Job。这是一个挂起函数,通常在协程内部使用。launchIn 让 Flow 可以在指定的协程作用域内被收集,适用于流的收集者不需要直接处理收集结果的情况。

示例:

flowOf(1, 2, 3)
    .onEach { value -> println(value) }
    .launchIn(scope)

shareIn

shareIn 是一个中间操作符,允许将 Flow 转换为一个共享的冷流(SharedFlow),并且可以在多个收集器之间共享。它通过缓存已经发出的数据,使多个协程能够同时收集同一个流的数据,而不必多次启动流。shareIn 通常用于需要让多个消费者收集相同的数据流的场景。其实它没有什么生产的,它的生产过程其实就是转发,它会把上游的Flow的每条数据都转发给它的FlowCollector,也就是下边示例中的{},并且在它多次调用collect()的时候,每个collect()各自的FlowCollector,也就是{}里边的参数,都会收到这个统一的上游的Flow里边发送的数据,这实际上就是发送流程和数据收集流程分开了,从这个角度来看,它不像是传统的Flow,而更像是Channel(数据发送与读取是分开的)而与Channel不同的是,channel的数据是瓜分的,而SharedFlow则会将每条数据都会发送到每一个进行中的collect去。

示例:

val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flow {
  emit(1)
  delay(1000)
  emit(2)
  delay(1000)
  emit(3)
}
// 下边的两个分别在两个协程中启动的SharedFlow都会收到完整的数据。
val sharedFlow = flow1.shareIn(scope, SharingStarted.Eagerly/*立即开始生产数据*/, 1)
scope.launch {
  delay(500)
  // 其实它没有什么生产的,它的生产过程其实就是转发,
  // 他会把上游的Flow的每条数据都转发给它的FlowCollector,也就是下边的{},
  // 并且在它多次调用collect()的时候,每个collect()各自的FlowCollector,
  // 也就是{}里边的参数,都会收到这个统一的上游的Flow里边发送的数据,
  // 这实际上就是发送流程和数据收集流程分开了,
  // 从这个角度来看,它不像是传统的Flow,而更像是Channel(数据发送与读取是分开的)
  // 而与Channel不同的是,channel的数据是瓜分的,
  // 而SharedFlow则会将每条数据都会发送到每一个进行中的collect去。
  sharedFlow.collect {
    println("SharedFlow in Coroutine 1: $it")
  }
}
// Channel: hot
// FLow: cold
scope.launch {
  delay(1500)
  sharedFlow.collect {
    println("SharedFlow in Coroutine 2: $it")
  }
}

/*
scope.launch {
  flow1.collect {
    println("flow2 - 1: $it")
  }
}
scope.launch {
  flow1.collect {
    println("flow2 - 2: $it")
  }
}
*/
// 输出
SharedFlow in Coroutine 2: 1
SharedFlow in Coroutine 1: 1
SharedFlow in Coroutine 2: 2
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 1: 3
SharedFlow in Coroutine 2: 3

普通的Flow是在每次collect的时候都完整的跑一次流程,从这些独立的流程里分别发送数据,而shareFlow是只跑一次流程,然后再每次collect的时候,都统一发送这个流程里边的数据,有什么区别?看起来效果似乎是一样的,如果像上边这样完全没有延迟的时候确实没有区别,但是如果将代码修改成下边这种方式,就会出现差异了。

/*
scope.launch {
  delay(2500)
  flow1.collect {
    println("flow2 - 1: $it")
  }
}
scope.launch {
  delay(1500)
  flow1.collect {
    println("flow2 - 2: $it")
  }
}
*/
val sharedFlow = flow1.shareIn(scope, SharingStarted.Eagerly, 1)
scope.launch {
  delay(500)
  sharedFlow.collect {
    println("SharedFlow in Coroutine 1: $it")
  }
}
// Channel: hot
// FLow: cold
scope.launch {
  delay(1500)
  sharedFlow.collect {
    println("SharedFlow in Coroutine 2: $it")
  }
}
// flow输出:
flow2 - 2: 1
flow2 - 1: 1
flow2 - 2: 2
flow2 - 1: 2
flow2 - 2: 3
flow2 - 1: 3

shareFlow输出:
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 1: 3
SharedFlow in Coroutine 2: 3

为什么两个输出会有差别呢?因为在上边的shareIn执行的时候,它背后的数据流就同步启动了,并且会在第一时间发送出第一条数据,但我们的shareFlow是在500ms之后才开始调用collect函数的,这样的话,它就错过了第一条数据,所以就无法打印第一个数据了,第二个shareFlow也是类似,它是在1500ms后启动的,会错过前两条数据。

主要区别:

  • 目的launchIn 是一个终端操作符,用于启动流的收集;shareIn 是一个中间操作符,用于将流转换为共享流。
  • 返回值launchIn 返回 Job,而 shareIn 返回 SharedFlow。且这个SharedFlow会拿到上游那个被启动的Flow的数据作为一个转发者,把上游的Flow的数据转发出去,转发给谁?转发给手机他的FlowCollector
  • 使用场景launchIn 用于简单的流收集操作,而 shareIn 用于需要多个协程共享同一个流的数据的情况。
  • shareIn并不一定是在第一时间启动Flow的收集,这个收集的启动时间可以通过参数来控制。

这两个操作符在复杂的流处理场景中可以结合使用,shareIn 让流的数据可以在多个消费者之间共享,而 launchIn 则可以启动流的收集过程。

冷流和热流

一般我们都说Channel是热的,Flow是冷的。为何?冷和热其实取决于数据的生产和收集是否是相关的,如果生产与收集独立,这个就是热的,反之,收集的时候才开始生产,就是冷的。而ShareFlow的活跃状态跟它是否正在被调用的collect函数收集数据是无关的。所以它的活跃状态是独立的,这就和Channl一样了,这样看起来它是不是更像是热的?虽然koltin官方说它是热的,但从技术本质上来说它是冷的。它也是在调用了collect之后才会启动自己的数据流,只不过它的数据流的生产逻辑比较特别,它是把上游的flow转发给下游,而这个上游的flow,它的流程跟下边的这个sharedFlow的collect是否调用是无关的,这样造成的现象就是看起来好像SharedFlow是在被数据收集之前就启动数据流了。其实他也是在调用collect之后才启动的。真正独立启动的,是它上游的它所依赖的flow数据流,所以说SharedFlow本质上是冷的。如果不从本质上说,它用起来其实更像是热的。

为了加深印象,我们再来举一个例子来说明这个问题:

Ticker.start()
val scope = CoroutineScope(EmptyCoroutineContext)
val flow2 = callbackFlow {
  Ticker.subscribe { trySend(it) }
  awaitClose()
}
scope.launch {
  delay(2500)
  flow2.collect {
    println("flow2 - 1: $it")
  }
}
scope.launch {
  delay(1500)
  flow2.collect {
    println("flow2 - 2: $it")
  }
}

object Ticker {
  private var time = 0
    set(value) { // Kotlin setter
      field = value
      subscribers.forEach { it(value) }
    }

  private val subscribers = mutableListOf<(Int) -> Unit>()

  fun subscribe(subscriber: (Int) -> Unit) {
    subscribers += subscriber
  }

  fun start() {
    GlobalScope.launch {
      while (true) {
        delay(1000)
        time++
      }
    }
  }
// 输出:
flow2 - 2: 2
flow2 - 1: 3
flow2 - 2: 3
flow2 - 2: 4
flow2 - 1: 4
flow2 - 2: 5
flow2 - 1: 5
flow2 - 2: 6
flow2 - 1: 6
flow2 - 2: 7
flow2 - 1: 7
flow2 - 1: 8
flow2 - 2: 8
flow2 - 2: 9
flow2 - 1: 9

在这个例子中,我们实现了一个每一秒钟更新的计时器,然后在flow中去收集计时器当前的值,从输出结果可以看到1这个值是没有收集到的,因为flow2的两个收集方式都存在着不同程度的延时,但是数据的生产确实一直在进行的,是一个完全独立的生产流程。如果是冷流,肯定能收集到所有的数据。

说SharedFlow是冷流,是因为它在调用collect的时候才开始收集,说它是热流,是因为它的数据并不是它自己生产的,来自上游其他的flow,因此是可能存在漏数据的情况的。

SharedFlow的作用

分拆数据生产与使用。

  1. 共享,多个收集流程共享同一套数据;
  2. 数据生产的提前启动,利用shareIn把flow转化成SharedFlow,然后在SharedFlow中调用collect就可以提前开始生产。
  3. 可能会漏掉数据,只能用于可以漏掉数据的场景,不需要从头开始监听数据,例如事件流,通常做事件流订阅,Flow一般做数据流订阅。

shareIn操作符

shareIn 是一个用于将冷流 (cold flow) 转换为热流 (hot flow) 的操作符。冷流在没有订阅者时不会发射数据,而热流则会持续发射数据,无论是否有订阅者。shareIn 可以帮将冷流转换为多个订阅者共享的数据流。

参数说明

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T> {
    val config = configureSharing(replay)
    val shared = MutableSharedFlow<T>(
        replay = replay,
        extraBufferCapacity = config.extraBufferCapacity,
        onBufferOverflow = config.onBufferOverflow
    )
    @Suppress("UNCHECKED_CAST")
    val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
    return ReadonlySharedFlow(shared, job)
}
  • scope: 定义了在什么作用域中这个热流会运行。通常是某个 CoroutineScope

  • started: 指定SharedFlow背后的那个生产数据的flow的启动事件,有以下几种常用选项:

    • SharingStarted.Lazily: 第一个SharedFlow调用collect的时候才启动。
    • SharingStarted.Eagerly: 在创建时立即启动流。
    • SharingStarted.WhileSubscribed(): 复杂化的lazily,不经是在第一次订阅的时候启动上游的数据流,而且在下游的所有订阅全部都结束之后,他还会把上游的Flow的生产过程也结束掉,如果在这之后又来了新的订阅者,它还会重启上游的数据流。 仅当有活跃订阅者时才运行,所有订阅者取消后会停止。
  • replay: 指定应为新的订阅者重放的值的数量,有点类似于buffer(用来处理上游生产的数据大于下游消费数据速度的时候如何处理这些过量的数据的一种概念。)。如果设置为1,相当于缓冲区的大小就是1,且后来的值会替换到最早来到的值。

fun main() = runBlocking<Unit> {
  val scope = CoroutineScope(EmptyCoroutineContext)
  val flow1 = flow {
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
  }
  val sharedFlow = flow1.shareIn(scope, SharingStarted.Eagerly)
  scope.launch {
    delay(500)
    sharedFlow.collect {
      println("SharedFlow in Coroutine 1: $it")
    }
  }
  scope.launch {
    delay(1500)
    sharedFlow.collect {
      println("SharedFlow in Coroutine 2: $it")
    }
  }
  delay(10000)
}
// 输出:
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 2: 3
SharedFlow in Coroutine 1: 3

调整缓冲区的大小是1后,为什么Coroutine 2依然收不到1这个数据,这是因为缓冲区只能暂存一个数据,而在Coroutine 2启动收集的时候,他前边已经发送了1和2这两个数据,缓冲区只能把最新的2暂存起来。

val sharedFlow = flow1.shareIn(scope, SharingStarted.Eagerly, 1)

// 输出:
SharedFlow in Coroutine 1: 1
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 2: 2
SharedFlow in Coroutine 1: 3
SharedFlow in Coroutine 2: 3

调整缓冲区的大小是2后,就可以收到所有的数据了:

val sharedFlow = flow1.shareIn(scope, SharingStarted.Eagerly, 2)
// 输出:
SharedFlow in Coroutine 1: 1
SharedFlow in Coroutine 2: 1
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 2: 2
SharedFlow in Coroutine 1: 3
SharedFlow in Coroutine 2: 3

不过buffer与 replay,像归像,他们其实不一样,不一样在哪里呢?我们注意到,SharedFlow的数据是在多次collect之间共享的,如果把第二个collect的延时再加大一点,加到5秒,那么这个时候第一个collect都已经把数据都消费完了,第二个collect才开始启动?那这样的话第二个collect还能收到数据吗?

scope.launch {
  delay(5000)
  sharedFlow.collect {
    println("SharedFlow in Coroutine 2: $it")
  }
}
// 输出:
SharedFlow in Coroutine 1: 1
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 1: 3
// 下边这两条几乎瞬间到达,因为缓冲区存在这两个数据了。
SharedFlow in Coroutine 2: 2
SharedFlow in Coroutine 2: 3

是能收到的,但是我们知道,缓冲的目的是因为上游生产太快,导致下游无法及时消费,所以才需要暂存这些来不及消费的数据,但是上边这个例子中,数据是已经被消费过了的。消费过了,还需要缓冲吗?是不需要的,可是我们再第二个collect里边依然拿到了数据,所以我们可以总结出区别了:

  • buffer:是用于生产速度大于消费速度的场景
  • SharedFlow:除了上述的这种典型的缓存功能外,对于已经消费的数据依然缓存下来,用来给后边订阅的collect使用。除了要把下游来不及消费的数据缓冲一下之外,还需要把消费过的数据页缓冲下来,一旦来了新的订阅,就把这些数据传递过去。所以这种能力(用了还不扔的效果)称之为cache更合适,就是缓存,因此,称SharedFlow是具有缓冲+缓存能力的组件是完全合适的,它在数据来不及消费的时候先把数据缓冲下来,缓冲的大小就是replay的大小,而对于已经使用完的数据,他也会继续缓存下来,缓存大小也是replay的值,等有了新的订阅的时候就直接把这个值给发送出去。这就是replay,一个缓冲+缓存的双功能参数。

例如,对上边的例子做修改,将 SharingStarted.Eagerly 调整为 WhileSubscribed(), 当第一个结束之后,就会触发重新的生产流程。而不是和lazily一样,和第一个collect共享数据流,当然了,就算是共享,即便上一个生产流程已经结束了,它还是可以去使用上一个数据流的缓存。这就是WhileSubscribed()的效果。为什么它是一个函数而不是对象呢?因为它还有参数可以配置。

fun main() = runBlocking<Unit> {
  val scope = CoroutineScope(EmptyCoroutineContext)
  val flow1 = flow {
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
  }
  val sharedFlow = flow1.shareIn(scope, SharingStarted.WhileSubscribed(), 2)
  scope.launch {
    delay(1500)
    sharedFlow.collect {
      println("SharedFlow in Coroutine 1: $it")
    }
  }
  scope.launch {
    delay(5000)
    sharedFlow.collect {
      println("SharedFlow in Coroutine 2: $it")
    }
  }
  delay(10000)
}
// 输出:
SharedFlow in Coroutine 1: 1
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 1: 3
SharedFlow in Coroutine 2: 2
SharedFlow in Coroutine 2: 3

咦?输出的数据不还是缓存吗?似乎并没有重启整个flow生产线啊。这是为什么呢?其实第一个collect根本没有结束,这里就要提到SharedFlow的流程如何结束的问题了。SharedFlow 的订阅流程并不会自动结束。确切的说是他的collect函数不会随着它上游的Flow的生产流程的结束而一起结束。例如上边的例子中flow1生产完数据就结束了。而下边的sharedFlow,它的每次订阅并不会随着上游flow的结束而一起结束,具体的说就是这些collect会永远运行而不会返回的。就算是上游的数据发送完了。下游的collect也会一直运行而不返回,SharedFlow 重写了Flow的collect函数,返回值改成了Nothing,而在flow里边,它的返回值是Unit,一个函数返回Nothing,表示它永远也不会返回了,要么抛异常结束,要么永远运行下去。由于collect是一个挂起函数,我们可以利用协程的取消方式,来实现对协程的取消,进而结束挂起函数,当然,作为挂起函数,我们需要主动的去配合协程的结束才行。这样才能真正的将挂起函数的逻辑终结掉。

public interface SharedFlow<out T> : Flow<T> {
    override suspend fun collect(collector: FlowCollector<T>): Nothing
}

// collect的实现类。
@Suppress("UNCHECKED_CAST")
override suspend fun collect(collector: FlowCollector<T>): Nothing {
    val slot = allocateSlot()
    try {
        if (collector is SubscribedFlowCollector) collector.onSubscription()
        val collectorJob = currentCoroutineContext()[Job]
        while (true) {
            var newValue: Any?
            while (true) {
                newValue = tryTakeValue(slot) // attempt no-suspend fast path first
                if (newValue !== NO_VALUE) break
                awaitValue(slot) // await signal that the new value is available
            }
            // 监听执行当前collect的协程是否还存活,没有存活,就抛出CancellationException异常,结束掉整个collect函数。
            collectorJob?.ensureActive()
            collector.emit(newValue as T)
        }
    } finally {
        freeSlot(slot)
    }
}

如果我们对上边的例子的逻辑增加以下代码:

fun main() = runBlocking<Unit> {
  val scope = CoroutineScope(EmptyCoroutineContext)
  val flow1 = flow {
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
  }
  val sharedFlow = flow1.shareIn(scope, SharingStarted.WhileSubscribed(), 2)
  scope.launch {
    val parent = this
    // 在第一个collect执行4秒之后,取消当前的协程,会发生什么呢?
    launch {
      delay(4000)
      parent.cancel()
    }
    delay(1500)
    sharedFlow.collect {
      println("SharedFlow in Coroutine 1: $it")
    }
  }
  scope.launch {
    delay(5000)
    sharedFlow.collect {
      println("SharedFlow in Coroutine 2: $it")
    }
  }
  delay(10000)
}
// 输出:
SharedFlow in Coroutine 1: 1
SharedFlow in Coroutine 1: 2
SharedFlow in Coroutine 1: 3
SharedFlow in Coroutine 2: 2
SharedFlow in Coroutine 2: 3
SharedFlow in Coroutine 2: 1
SharedFlow in Coroutine 2: 2
SharedFlow in Coroutine 2: 3

在第一个collect执行4秒之后,取消当前的协程,会发生什么呢?可以看到发送流程确实重启了,但是为什么第一个collect还是会多收到一次2.3数据?这是因为缓存并没有因为重启而丢弃,还是会先发送缓存数据,再从头开始发送上游新一轮的流程的数据。这就是WhileSubscribed的效果,注意,它一定是在所有的collect结束之后,再新来的collect才会重启上游的,不过不仅是重启上游的生产, 也会导致上游正在进行中的生产立即结束掉,为什么?为了省资源。如果总有没有结束的collect。那么WhileSubScribed不会导致上游数据发送的重启。

WhileSubscribed的函数参数

public fun WhileSubscribed(
    stopTimeoutMillis: Long = 0,
    replayExpirationMillis: Long = Long.MAX_VALUE
): SharingStarted =
    StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)

参数解析

  1. stopTimeoutMillis: Long

    • 这个参数定义了在最后一个订阅者取消订阅后,Flow 继续保持活动状态的时间(以毫秒为单位)。
    • 如果在 stopTimeoutMillis 时间内没有新的订阅者订阅,Flow 将停止发射数据。
    • 默认值为 0 毫秒,这意味着一旦所有订阅者取消订阅,Flow 将立即停止。

    示例

    • stopTimeoutMillis = 0: 立即停止(默认行为)。
    • stopTimeoutMillis = 5000: 在所有订阅者取消订阅后,Flow 将保持活跃5秒钟,如果在这5秒内没有新的订阅者,它才会停止。
  2. replayExpirationMillis: Long

    • 这个参数定义了重放缓存中数据项的过期时间(以毫秒为单位), 仅针对缓存。
    • 也就是说,在这个时间段内,新的订阅者将能够接收到过去发射的最后一个值(如果有的话)。
    • 默认值为 Long.MAX_VALUE,这意味着缓存的数据项将永不过期,新的订阅者始终会接收到最后发射的值。

在最后一个collect结束,并且设置的stopTimeoutMillis也超时之后,如果再过replayExpirationMillis的时间,还是没有新的collect函数调用,那么刚刚缓存的数据就会被丢弃掉。如果没有的话,那么这些缓存数据就会被发送到新的collect中区,然后再被其他数据顶替掉。

**示例**-   `replayExpirationMillis = Long.MAX_VALUE`: 数据项永不过期(默认行为)。
-   `replayExpirationMillis = 10000`: 在流被订阅的10秒内,新的订阅者会接收到最后发射的值。超过10秒后,缓存的数据项将失效。

使用示例:

假设有一个共享的 Flow,并希望它在所有订阅者取消订阅后,延迟5秒才停止,同时缓存的数据项在10秒后过期:

val sharedFlow = someFlow.shareIn(
    scope = scope,
    started = SharingStarted.WhileSubscribed(
        stopTimeoutMillis = 5000, // 5秒后停止
        replayExpirationMillis = 10000 // 10秒后缓存失效
    ),
    replay = 1 // 重放最后一个数据项
)

总结

  • stopTimeoutMillis: 控制 Flow 在所有订阅者取消订阅后保持活跃的时间。
  • replayExpirationMillis: 控制重放缓存的过期时间。

SharingStarted.WhileSubscribed 的主要使用场景是在有订阅者时才希望启动数据流动,并在没有订阅者时自动停止流动。在许多情况下,流的数据生成可能会占用大量资源(例如网络请求、数据库访问或复杂计算)。WhileSubscribed 可以确保流只有在有实际需求时才会启动,从而避免在没有订阅者时浪费资源。这种机制能够有效地节省资源、优化性能,特别适用于需要动态响应订阅状态的应用场景。

MutableSharedFlow

除了shareIn的另一种创建SharedFlow的方式。shareIn是把普通的flow数据流转化成flow的,而MutableSharedFlow()函数是凭空创建一个MutableSharedFlow的对象出来,它可以用任何的外部数据源来发送数据,发生方式就是调用MutableSharedFlow的emit函数,所以它并不会限制有几个数据源。

典型用法

1. 事件总线

MutableSharedFlow 非常适合作为事件总线,用于在不同的组件之间传递消息或事件。

// 创建一个事件总线
val eventBus = MutableSharedFlow<String>()

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

// 发射事件
eventBus.emit("Event 1")
eventBus.emit("Event 2")

2. 共享状态

你可以使用 MutableSharedFlow 来共享某种状态,并在不同的订阅者之间同步该状态。

val stateFlow = MutableSharedFlow<Int>(replay = 1) // 缓存最后一个状态值

// 发射状态
stateFlow.emit(1)
stateFlow.emit(2)

// 新的订阅者将会收到最后一个状态值 (即 2)
stateFlow.collect { state ->
    println("Received state: $state")
}

3. 处理无粘性事件

假设在 UI 中需要处理点击事件或其他一次性事件,而不需要新订阅者了解过去的事件,这时候 MutableSharedFlow 的无重放特性(replay = 0)就很适合。

val clickEvents = MutableSharedFlow<Unit>()

// 处理点击事件
clickEvents.collect {
    println("Button clicked!")
}
// 发射点击事件
clickEvents.emit(Unit)

在之前的章节中,我们使用过SharedFlow,它的数据源的来源是flow,那么如果我们想要外部数据源,或者说我想从这个flow之外的其他地方发送数据或事件,应该怎么办呢?首先我们需要知道,为什么我们会需要外部数据源?我们真的需要SharedFlow有外部数据源吗?事件流,典型的一个场景就是UI事件流,比如,用户对按钮的点击事件的订阅那么如果我们有一个SharedFlow的对象,我们希望用户通过点击这个按钮后触发emit函数,而不是像之前的例子中,数据只能由flow自己内部发送。但我们知道,emit只能在flow内部自己调用,因为数据流,数据从内部生产才符合常理,外部生产是不符合常理的。但是SharedFlow是不一样的,它是一个事件流工具,那么就像之前所述,点击订阅这种能力,从外部发送事件的需求就很合理。flow没有这个功能,但是SharedFlow有这个功能。调用MutableSharedFlow()函数就可以了。

val clickFlow = MutableSharedFlow<String>()
scope.launch {
  // 可以直接调用emit了。
  clickFlow.emit("Hello")
}
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {

public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {

public fun interface FlowCollector<in T> {
    public suspend fun emit(value: T)
}

MutableSharedFlow 是SharedFlow的子接口,同时也是FlowCollector的子接口,让他有直接调用emit函数的能力。所以本质上MutableShareFlow是一个更强大灵活的SharedFlow对象。其实我们进入shareIn的源码看看,会发现shareIn也是利用MutableSharedFlow做的下层支持:

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T> {
    val config = configureSharing(replay)
    // shareIn也是利用了MutableSharedFlow来实现的。
    val shared = MutableSharedFlow<T>(
        replay = replay,
        extraBufferCapacity = config.extraBufferCapacity,
        onBufferOverflow = config.onBufferOverflow
    )
    @Suppress("UNCHECKED_CAST")
    val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
    return ReadonlySharedFlow(shared, job)
}

但是,MutableSharedFlow是不是少了不能从内部生产的能力呢?这个我们就要从更高层次来考虑了,我们从框架层面考虑为什么flow要被设计成从内部发送数据?因为它是数据流,数据流本来就是提前设计好规则,然后执行的时候按照这个规则把一条条数据发送出来。本来就不需要从外部发送,而在不需要外部发送的情况下,如果提供了外部发送的能力,那么共容易让开发者写出错误代码,把程序弄错。但是SharedFlow它已经是事件流了,事件流我们也知道,它天然就是需要从各个地方发送数据的,那么flow这个限制就没有必要了。所以MutableSharedFlow这个对象就可以让我们在任何协程中手动调用emit来发送数据,但是只能从外部了,而失去了内部发送的能力,这个取舍是基于什么考虑呢?会有什么不良后果?又如何解决呢?我们何时需要内部外部都能发送数据呢?

我们以一个示例来回答上边的问题:

val flow1 = flow {
  emit(1)
  delay(1000)
  emit(2)
  delay(1000)
  emit(3)
}
val clickFlow = MutableSharedFlow<String>()
val readonlyClickFlow = clickFlow.asSharedFlow()
val sharedFlow = flow1.shareIn(scope, SharingStarted.WhileSubscribed(), 2)
scope.launch { 
  sharedFlow.collect {
    println("SharedFlow in Coroutine 1: $it")
  }
}

我们所谓的内部,是从谁的内部?是上游的flow的内部,所以对于下游的SharedFlow来说,从它的内部发送数据,更确切的其实是上游的flow发送数据,根本就不是下游的SharedFlow内部发送的,因此,对于SharedFlow来说,无论是上游的shareIn,还是从任何协程通过MutableSharedFlow的emit发送数据,其实都是外部数据, MutableSharedFlow 不能从上游的flow里边生产数据,并不是能力的缺失,因为从上游的flow里边生产,实际上还是从SharedFlow的外部生产。如果不纠结在flow的内部生产数据,而是想用 MutableSharedFlow 实现flow函数+shareIn的效果。应该怎么做?

val sharedFlow = flow1.shareIn(scope, SharingStarted.WhileSubscribed(), 2)
scope.launch { 
  // 下边这种方式等同于上边的flow1
  clickFlow.emit(1)
  delay(1000)
  clickFlow.emit(2)
  delay(1000)
  clickFlow.emit(3)
  val parent = this
  launch {
    delay(4000)
    parent.cancel()
  }
  delay(1500)
  sharedFlow.collect {
    println("SharedFlow in Coroutine 1: $it")
  }
}

我们使用MutableSharedFlow时,其实已经不需要上游的数据了,直接使用emit函数就可以了。所有MutableSharedFlow对比shareIn并不是从内部发送数据变成了外部发送数据,而是变成了从只能上游的flow发送数据到任何协程都可以发送数据。所以它其实是完全的功能范围的增加。

shareIn和MutableSharedFlow的选择

他们本质上是一个东西,需要谁用谁就行。如果我们需要提供一个事件流的时候,就使用MutableSharedFlow,而如果已经有了一个生产事件流的Flow,那就不用再费事去创建一个MutableSharedFlow去写生产代码了,直接用shareIn转化一下即可。

MutableSharedFlow的参数

MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
  • replay: Int (默认值:0)

    • 定义:与shareIn定义相同,replay 参数用于指定新订阅者可以接收到的最近发射值的数量。它决定了当一个新的订阅者订阅 SharedFlow 时,可以重放多少过去的值。

    • 使用场景

      • replay = 0:不重放任何先前的值(只发射新数据)。适合事件驱动场景,比如用户点击事件。
      • replay = 1:重放最后一个值,常用于状态共享场景(新订阅者会收到最新的状态)。
      • replay > 1:重放多个值,适合需要传递一些历史数据的场景。
  • extraBufferCapacity: Int (默认值:0)

    • 定义extraBufferCapacity 参数用于定义除 replay 容量之外的额外缓冲区大小。如果流发射的数据比订阅者处理的速度快,这个缓冲区可以存储额外的未处理值,只能增加缓冲(buffer),不能增加缓存(cache),在数据消费完之后,继续存下来给后边订阅者使用的是re而lay,而如果上游生产数据过快,导致下游来不及消费的,最多可以存replay+extraBufferCapacity个。

    • 使用场景

      • extraBufferCapacity = 0:没有额外缓冲区。发射的数据如果超过处理能力,就会根据 onBufferOverflow 策略进行处理。
      • extraBufferCapacity > 0:可以暂存一定数量的未处理值,这在需要处理突发数据或高吞吐量场景中非常有用。
  • onBufferOverflow: BufferOverflow (默认值:BufferOverflow.SUSPEND)

    • 定义onBufferOverflow 参数定义了在缓冲区满时的行为策略。与buffer操作符以及Channe的缓冲溢出策略是类似的,它的类型以及每个值的效果都是一样的,但是需要注意,这个策略只针对缓冲,不针对缓存,缓存这个功能本来就是数据使用完之后才存的。所以在没有任何订阅的时候,也就是没有任何正在进行中的collect的时候,缓存溢出策略就直接被忽略了,在没有订阅的时候就没有缓冲了,但是就算是没有订阅,缓存也依然还是存在的,缓存并不受onBufferOverflow的影响。这也很好理解,缓冲本来就是上游生产太快而下游来不及消费导致的,下游已经咋消费了,所以我们必须要对过多生产的数据做出抉择,但是缓存不一样,还没有适用法来使用,因此我们保留最新的就行了。它有三个可选值:

      • BufferOverflow.SUSPEND:默认策略,发射器会挂起(暂停)直到有空间。
      • BufferOverflow.DROP_OLDEST:丢弃缓冲区中最老的值,腾出空间来保存新值。
      • BufferOverflow.DROP_LATEST:丢弃新发射的值,而不是保存到缓冲区。
    • 使用场景

      • BufferOverflow.SUSPEND:适合需要严格确保所有值都被处理的场景,发射器可能会等待。
      • BufferOverflow.DROP_OLDEST:适合只关心最新数据的场景,比如实时数据流,旧数据被抛弃。
      • BufferOverflow.DROP_LATEST:适合只关注先到数据的场景,新数据被丢弃。

asSharedFlow()

把 MutableSharedFlow 转化成SharedFlow的函数,何时需要?在我们需要把MutableSharedFlow暴露给外部订阅,但并不希望外部也来发送数据的时候。我们就可以用它来创建一个SharedFlow来返回了。返回一个只能读,不能写的SharedFlow。

val readonlyClickFlow = clickFlow.asSharedFlow()

StateFlow

StateFlow 是 Kotlin Coroutines 库中用于状态管理的一种 Flow,它是一个特化的 SharedFlow,专门用于表示和管理可变状态,只保留当前最新的一个事件的事件流。StateFlow 总是包含一个当前值,并且所有新的订阅者都会立即收到这个当前值。ShareFlow是特殊的flow,将能力收窄到了事件流订阅,而StateFlow是特殊的SharedFlow,将能力收窄到了状态订阅,SharedFlow我们知道是可以设置缓存和缓冲的,而StateFlow的缓冲与缓存的大小都是1,并且它不光能缓存,还能让外界直接访问这个缓存的对象。就是value对象,一个只缓存一条数据的事件流,并且这个缓存的数据还能直接访问,那这不就是一个带有监听功能的状态吗?

public interface StateFlow<out T> : SharedFlow<T> {
    /**
     * 最新的那条事件的数据值。
     */
    public val value: T
}

状态订阅的几种方式

状态的订阅的流程无非就是下边这样的一个标准:把状态包装起来,然后订阅状态,以及更新状态。

  • RXJava实现的订阅
val name = BehaviorSubject.createDefault("ZZZZ")
name.subscribe{
  println(it)
}
name.value
name.onNext("XXXX")
  • LiveData实现的订阅
var name = MutableLiveData("CCCC")
name.value
name.observeForever{
  println(it)
}
  • MutableStateFlow实现的订阅
val name = MutableStateFlow("AAAA")
val flow1 = flow {
  emit(1)
  delay(1000)
  emit(2)
  delay(1000)
  emit(3)
}
// 订阅状态
scope.launch {
  name.collect {
    println("State: $it")
  }
}
// 更新状态
scope.launch {
  delay(2000)
  name.emit("扔物线")
}
  • 自己实现观察者与被观察者逻辑完成订阅

StateFlow 的特点

StateFlow也有一个跟shareIn()类似的操作符,叫stateIn,把flow转化成stateFlow。stateIn只有一个参数,这是因为它不需要你去手动的修改缓冲与缓存的尺寸以及缓冲溢出的策略,而stateFlow因为缓冲与缓存的大小固定为1,所以不需要调节了,并且缓冲溢出策略也是固定的,就是抛弃旧数据。

public suspend fun <T> Flow<T>.stateIn(scope: CoroutineScope): StateFlow<T> {
}

另外StateFlow还有一个asStateFlow()函数,类似于SharedFlow中asSharedFlow(),用来将stateFlow转化成制度的StateFlow的。让StateFlow变成只读的,在对外暴露的时候把写数据的功能给藏起来。

  1. 持有状态

    • StateFlow 始终持有一个最新的状态值,并且可以被多个订阅者观察。当状态更新时,所有订阅者都会被通知并收到新的状态值。
  2. 冷流的行为

    • 虽然 StateFlow 是一种热流,但它的行为更接近冷流。这意味着即使没有订阅者,状态也会保持并可以被观察。
  3. 单一状态值

    • StateFlow 只保留一个状态值,新的状态会覆盖旧的状态,因此它不会重放历史状态,而是仅仅保持最新的状态值。
  4. 自动重放

    • 任何时候订阅 StateFlow 的订阅者都会立即收到当前的状态值。这使得它非常适合在 UI 层用来绑定和响应状态变化。
  5. LiveData 的相似性

    • StateFlow 的设计与 Android 的 LiveData 类似,但 StateFlow 更强大且适用于非 Android 平台,能够在 Kotlin 多平台项目中使用。

创建 StateFlow

创建 StateFlow 可以使用 MutableStateFlow,它是 StateFlow 的可变版本,你可以通过它来更新状态。

val stateFlow = MutableStateFlow(0) // 初始状态为 0

主要方法和属性

value: T

  • StateFlow 持有的当前状态值,可以通过 value 属性访问或更新它(在 MutableStateFlow 中)。
val currentState = stateFlow.value // 获取当前状态
stateFlow.value = 1 // 更新状态

collect 方法

  • StateFlow 是一种 Flow,因此你可以使用 collect 方法来订阅和收集状态更新。
stateFlow.collect { value ->
    println("Received state update: $value")
}

subscriptionCount

  • StateFlow 提供了一个 subscriptionCount 属性,它是一个 StateFlow<Int>,可以用来监控当前有多少订阅者在观察这个状态流。
stateFlow.subscriptionCount.collect { count ->
    println("Current subscription count: $count")
}

典型使用场景

1. UI 状态管理

StateFlow 非常适合在应用中管理 UI 状态。它可以用来表示屏幕的当前状态,并确保每次屏幕状态发生变化时,UI 都能够及时更新。

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(MyUiState())
    val uiState: StateFlow<MyUiState> = _uiState

    fun updateState(newState: MyUiState) {
        _uiState.value = newState
    }
}

在 UI 层,可以订阅 uiState 来更新界面:

viewModel.uiState.collect { state ->
    // 更新 UI
}

表单输入管理

你可以使用 StateFlow 来管理用户在表单中的输入。每次用户输入时,StateFlow 都会更新,UI 组件可以响应这些变化并更新显示。

val usernameFlow = MutableStateFlow("")

fun onUsernameChanged(newUsername: String) {
    usernameFlow.value = newUsername
}

在非 Android 平台使用

LiveData 不同,StateFlow 是平台无关的,这意味着可以在服务器端或其他非 Android 平台项目中使用它来管理状态。

总结

StateFlow 用于管理和共享状态。它适用于需要持续跟踪状态变化并响应这些变化的场景,如 UI 状态管理、输入处理等。StateFlow 是 Kotlin Coroutines 提供的现代化解决方案,比传统的 LiveData 更灵活且无平台限制,非常适合多平台开发。