Flow 是一个异步数据流,它可以顺序地发出数据,通过流上的一些中间操作得出结果;若出错可抛出异常。这些 “流上的中间操作” 包括但不限于 map
、filter
、take
、zip
等等方法。这些中间操作是链式的,可以在后面再次添加其他操作方法,并且也不是挂起函数,它们只是构建了一条链式的操作并实时返回结果给后面的操作步骤。
组成
Flow 一般包含三个部分:
1)提供方:负责生成数据并添加到 Flow 中,得益于协程,Flow 可以异步生成数据;
2)中介(可选):可对 Flow 中的值进行操作、修改;也可修改 Flow 本身的一些属性,如所在线程等;
3)使用方:接收并使用 Flow 中的值。
提供方:生产者,使用方:消费者,典型的生产者消费者模式。
冷流与热流
- 冷流(Cold Flow):在数据被使用方订阅后,即调用
collect
方法之后,提供方才开始执行发送数据流的代码,通常是调用emit
方法。即不消费,不生产,多次消费才会多次生产。使用方和提供方是一对一的关系。 - 热流(Hot Flow):无论有无使用方,提供方都可以执行发送数据流的操作,提供方和使用方是一对多的关系。热流就是不管有无消费,都可生产。
SharedFlow
就是热流的一种,任何流也可以通过 stateIn
和 shareIn
操作转化为热流,或者通过 produceIn
操作将流转化为一个热通道也能达到目的。
常用的操作符
Flow 的使用依赖于众多的操作符,这些操作符可以大致地分为 中间操作符 与 末端操作符 两大类。中间操作符是流上的中间操作,可以针对流上的数据做一些修改,是链式调用。中间操作符与末端操作符的区别是:中间操作符是用来执行一些操作,不会立即执行,返回值还是个 Flow;末端操作符就会触发流的执行,返回值不是 Flow。
一个完整的 Flow 是由 Flow 构建器
、Flow 中间操作符
、Flow 末端操作符
组成,如下示意图所示:
构造器
flow{...}
这个方法可以在其内部顺序调用 emit
方法或 emitAll
方法从而构造一个顺序执行的 Flow。emit
是发射单个值;emitAll
是发射一个流,这两个方法分别类似于 list.add(item)
、list.addAll(list2)
方法。flow {···}
方法的源码如下:
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)
需要额外注意的是,flow
后面的 lambda 表达式是一个挂起函数,里面不能使用不同的 CoroutineContext
来调用 emit
方法去发射值。因此,在 flow{...}
中不要通过创建新协程或使用 withContext
代码块在另外的 CoroutineContext
中调用 emit
方法,否则会报错。如果确实有这种需求,可以使用 channelFlow
操作符。
val testFlow = flow {
emit(23)
// withContext(Dispatchers.Main) { // error
// emit(24)
// }
delay(3000)
emitAll(flowOf(25,26))
}
asFlow()
asFlow()
操作符。是集合的扩展方法,可将其他数据转换成 Flow,例如 Array
的扩展方法:
public fun <T> Array<T>.asFlow(): Flow<T> = flow {
forEach { value ->
emit(value)
}
}
不仅 Array
扩展了此方法,各种其他数据类型的数组都扩展了此方法。所以集合可以很方便地构造一个 Flow。
flowOf()
flowOf()
方法。用于快速创建流,类似于 listOf()
方法,下面是它的源码:
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
for (element in elements) {
emit(element)
}
}
注意到 Flow 初始化的时候跟其他对象一样,作用域在哪儿都可以,但 collect
收集的时候就需要放在协程里了,因为 collect
是个挂起函数:
val testFlow = flowOf(65,66,67)
lifecycleScope.launch {
testFlow.collect {
println("输出:$it")
}
}
//打印结果:
//输出:65
//输出:66
//输出:67
channelFlow{...}
channelFlow {···}
方法。这个方法就可以在内部使用不同的 CoroutineContext
来调用 send
方法去发射值,而且这种构造方法保证了线程安全也保证了上下文的一致性,源码如下:
public fun <T> channelFlow(@BuilderInference block: suspend ProducerScope<T>.() -> Unit): Flow<T> =
ChannelFlowBuilder(block)
一个简单的使用例子:
val testFlow1 = channelFlow {
send(20)
withContext(Dispatchers.IO) { //可切换线程
send(22)
}
}
lifecycleScope.launch {
testFlow1.collect {
println("输出 = $it")
}
}
MutableStateFlow
MutableStateFlow
操作符,可以定义相应的构造函数去创建一个可以直接更新的热流,后面在StateFlow和ShareFlow会详细说明。
MutableSharedFlow
MutableSharedFlow
操作符,可以定义相应的构造函数去创建一个可以直接更新的热流,后面在StateFlow和ShareFlow会详细说明。
中间操作符
zip
顾名思义,zip中间操作符就是可以将两个 Flow 汇合成一个 Flow:
lateinit var testFlow1: Flow<String>
lateinit var testFlow2: Flow<String>
private fun setupTwoFlow() {
testFlow1 = flowOf("Red", "Blue", "Green")
testFlow2 = flowOf("fish", "sky", "tree", "ball")
CoroutineScope(Dispatchers.IO).launch {
testFlow1.zip(testFlow2) { firstWord, secondWord ->
"$firstWord $secondWord"
}.collect {
println("+++ $it +++")
}
}
}
// 输出结果:
//com.example.myapplication I/System.out: +++ Red fish +++
//com.example.myapplication I/System.out: +++ Blue sky +++
//com.example.myapplication I/System.out: +++ Green tree +++
//zip 方法声明:
public fun <T1, T2, R> Flow<T1>.zip(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R> = zipImpl(this, other, transform)
从 zip
方法的声明中可知,zip
方法的第二个参数就是针对两个 Flow 进行各种处理的挂起函数,也可如例子中写成尾调函数的样子,返回值是处理之后的 Flow。而且当两个 Flow 长度不一样时,最后的结果会默认剔除掉先前较长的 Flow 中的元素。所以 testFlow2
中的 “ball” 就被自动剔除掉了。
filter
用来将Flow中的数据进行筛选处理,然后交给下一个操作符
map
用来将 Flow 中的数据一个个拿出来做各自的处理,然后交给下一个操作符。
check
类似于一个检查站,满足括号内条件的数据可以通过,不满足则交给它的尾调函数处理,并且抛出异常。
onCompletion
Flow 最后的兜底器。无论 Flow 最后是执行完成、被取消、抛出异常,都会走到 onCompletion
操作符中,类似于在 Flow 的 collect
函数外加了个 try
,finally
。
catch
不用多说,专门用于捕捉异常的,避免程序崩溃。这里如果把 catch
去掉,程序就会崩溃。如果把 catch
和 onCompletion
操作符位置调换,则 onCompletion
里面就接收不到异常信息了。
末端操作符
collect
collect
操作符是个挂起函数,需要在协程作用域中调用;并且它是一个末端操作符,末端操作符就是实际启动 Flow 执行的操作符,这一点跟 RxJava 中的 Observable
对象的执行很像。Flow 在调用 collect
操作符收集流之前,Flow 构建器和中间操作符都不会执行
val testFlow2 = flow {
println("++++ 开始")
emit(40)
println("++++ 发出了40")
emit(50)
println("++++ 发出了50")
}
lifecycleScope.launch {
testFlow2.collect{
println("++++ 收集 = $it")
}
}
// 输出结果:
//com.example.myapplication I/System.out: ++++ 开始
//com.example.myapplication I/System.out: ++++ 收集 = 40
//com.example.myapplication I/System.out: ++++ 发出了40
//com.example.myapplication I/System.out: ++++ 收集 = 50
//com.example.myapplication I/System.out: ++++ 发出了50
每次到 collect
方法调用时,才会去执行 emit
方法,而在此之前,emit
方法是不会被调用的。这种 Flow 就是冷流。
reduce
reduce
也是一个末端操作符,它的作用就是将 Flow 中的数据两两组合接连进行处理,跟 Kotlin 集合中的 reduce
操作符作用相同。
private fun reduceOperator() {
val testFlow = listOf("w","i","f","i").asFlow()
CoroutineScope(Dispatchers.Default).launch {
val result = testFlow.reduce { accumulator, value ->
println("+++accumulator = $accumulator value = $value")
"$accumulator$value"
}
println("+++final result = $result")
}
}
//输出结果:
//com.example.myapplication I/System.out: +++accumulator = w value = i
//com.example.myapplication I/System.out: +++accumulator = wi value = f
//com.example.myapplication I/System.out: +++accumulator = wif value = i
//com.example.myapplication I/System.out: +++final result = wifi
两个值处理后得到的新值作为下一轮中的输入值之一,这就是两两接连进行处理的意思。
toList
toList
操作符也是一种末端操作符,可以将 Flow 返回的多个值放进一个 List
中返回,返回的 List
也可以自己设置。
stateIn
stateIn
操作符也是一种末端操作符,可以将Flow从冷流转为热流,后面在StateFlow和ShareFlow会详细说明。
shareIn
shareIn
操作符也是一个末端操作符,可以将Flow从冷流转为热流,后面在StateFlow和ShareFlow会详细说明。