文章目录
- [Flow 的创建](#Flow 的创建)
-
- [创建一个或一串数据转换成 Flow:flowOf()](#创建一个或一串数据转换成 Flow:flowOf())
- [转换成 Flow:asFlow()](#转换成 Flow:asFlow())
- [Channel 转换为 Flow:consumeAsFlow()、receiveAsFlow() 和 channelFlow()](#Channel 转换为 Flow:consumeAsFlow()、receiveAsFlow() 和 channelFlow())
- callbackFlow()
- [Flow 为什么不允许切换协程?](#Flow 为什么不允许切换协程?)
- [Flow 的操作符](#Flow 的操作符)
-
- [filter() 系列操作符](#filter() 系列操作符)
- [distinctUntilChanged() 和 distinctUntilChangedBy() 操作符](#distinctUntilChanged() 和 distinctUntilChangedBy() 操作符)
- [自定义 Flow 操作符](#自定义 Flow 操作符)
- timeout()、sample()、debounce()
- [drop()、take() 系列操作符](#drop()、take() 系列操作符)
- [map() 系列操作符](#map() 系列操作符)
- [transform() 系列操作符](#transform() 系列操作符)
- [withIndex() 操作符](#withIndex() 操作符)
- [reduce()、fold() 系列操作符](#reduce()、fold() 系列操作符)
- [onEach() 操作符](#onEach() 操作符)
- [chunked() 操作符](#chunked() 操作符)
- [catch() 操作符](#catch() 操作符)
- [retry()、retryWhen() 操作符](#retry()、retryWhen() 操作符)
- [onStart()、onCompletion()、onEmpty() 监听流程操作符](#onStart()、onCompletion()、onEmpty() 监听流程操作符)
- onStart()
- onCompletion()
- onEmpty()
- [flowOn() 操作符](#flowOn() 操作符)
- 总结
Flow 的创建
在前面的章节讲解 Flow 时创建 Flow 是直接用 flow() 函数创建,然后在 lambda 提供生产发送数据的处理:
java
fun main() = runBlocking {
val flow = flow {
emit(1)
}
}
其实 Flow 还提供了其他创建 Flow 的函数,让我们方便的把其他类型的对象直接转换成 Flow。
创建一个或一串数据转换成 Flow:flowOf()
flowOf() 需要提供一个或者一串数据,然后帮你创建一个 Flow 对象并依次的发送数据:
java
fun main() = runBlocking {
val flow = flowOf(1, 2, 3)
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("Flow: $it")
}
}
delay(10000)
}
转换成 Flow:asFlow()
asFlow() 是扩展函数,可以将 List、Set 等对象转换成 Flow:
java
fun main() = runBlocking {
val flow = listOf(1, 2, 3).asFlow()
// val flow = setOf(1, 2, 3).asFlow()
// val flow = sequenceOf(1, 2, 3).asFlow()
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("Flow: $it")
}
}
delay(10000)
}
Channel 转换为 Flow:consumeAsFlow()、receiveAsFlow() 和 channelFlow()
consumeAsFlow()、receiveAsFlow()
Channel 也可以转换成 Flow,但是它有两个函数 consumeAsFlow() 和 receiveAsFlow():
java
fun main() = runBlocking {
val channel = Channel<Int>()
val flow = channel.consumeAsFlow()
// val flow = channel.recieveAsFlow()
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("Flow: $it")
}
}
delay(10000)
}
上面的例子从上游看是由 Channel 生产数据,然后交给下游的 Flow,下游 Flow 直到每次调用 collect() 的时候才会发送数据。
简单理解就是上游 Channel 一直在发送数据,下游的 Flow 调用 collect() 才会释放数据否则就掐着不放数据到下游。
用 Channel 转换的 Flow 也有不同的地方:多个协程调用 collect() 会瓜分数据:
java
fun main() = runBlocking {
val channel = Channel<Int>()
val flow = channel.recieveAsFlow()
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("Flow - 1: $it")
}
}
scope.launch {
flow.collect {
println("Flow - 2: $it")
}
}
channel.send(1)
channel.send(2)
channel.send(3)
channel.send(4)
delay(10000)
}
输出结果:
Flow - 2:1
Flow - 1:2
Flow - 2:3
Flow - 1:4
用 Channel 转换的 Flow 虽然从整体流程上可以看成是 [热] 的也能看成是 [冷] 的,但从行为模式上整体流程还是 [热] 的,因为接收的数据不是相互独立的会瓜分 Channel 发送的数据。
consumeAsFlow() 和 receiveAsFlow() 的区别:
-
consumeAsFlow() 只能被消费一次,调用多次 collect() 会抛出异常
-
receiveAsFlow() 多次接收不会抛异常
channelFlow()
channelFlow() 相比上面提到的 consumeAsFlow() 和 receiveAsFlow() 是完全不同的:
-
consumeAsFlow() 和 receiveAsFlow() 是用一个现成的 Channel 作为数据源,所有 collect() 来共享消费瓜分 Channel 发送的数据
-
channelFlow() 是直到调用 collect() 才会创建 Channel,多次调用 collect() 就会创建多个 Channel,这些 Channel 的生产流程是互相隔离、各自独立的
channelFlow 的使用场景:
-
需要在 Flow 启动子协程
-
Flow 是不允许切换协程调用 emit(),而你有跨协程生产数据的需求
java
fun main() = runBlocking {
val flow = channelFlow {
// channelFlow 可以启动子协程
launch {
delay(2000)
send(2)
}
delay(1000)
send(1)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("channelFlow: $it")
}
}
delay(10000)
}
输出结果:
channelFlow:1
channelFlow:2
channelFlow 还支持与回调协作转换:
java
fun main() = runBlocking {
val flow = channelFlow {
gitHub.contributorsCall("square", "retrofit")
.enqueue(object: Callback<<List<Contributor>> {
override fun onResponse(call: Call<List<Contributor>>, response: Response<List<Contributor>>) {
trySend(response.body()!!) // 发送数据
close() // 要手动关闭 Channel
}
override fun onFailure(call: Call<List<Contributor>>, error: Throwable) {
cancel(CancellationException(error)) // 发生错误取消 Channel
}
})
// 处在协程环境,要等待回调执行完处理后再关闭
// channelFlow 提供了 awaitClose() 挂起协程
awaitClose()
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("channelFlow with callback: $it")
}
}
delay(10000)
}
callbackFlow()
实际上相比 channelFlow(),跟回调协作处理更适合的是 callbackFlow(),它和 channelFlow() 在使用上是完全一样的:
java
fun main() = runBlocking {
val flow = callbackFlow {
gitHub.contributorsCall("square", "retrofit")
.enqueue(object: Callback<<List<Contributor>> {
override fun onResponse(call: Call<List<Contributor>>, response: Response<List<Contributor>>) {
trySend(response.body()!!) // 发送数据
close() // 要手动关闭 Channel
}
override fun onFailure(call: Call<List<Contributor>>, error: Throwable) {
cancel(CancellationException(error)) // 发生错误取消 Channel
}
})
// 使用 callbackFlow 会强制要求调用 awaitClose() 否则会抛异常
awaitClose()
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("callbackFlow: $it")
}
}
delay(10000)
}
一般将回调 API 转换成协程使用的 suspendCancellationCoroutine();
suspendCancellationCoroutine() 与 callbackFlow() 的区别是:
-
suspendCancellationCoroutine() 能处理单次的回调切换到协程环境
-
callbackFlow() 可以处理连续的回调
callbackFlow() 可以看成是 Flow 版的 suspendCancellationCoroutine(),一个负责单次的回调,一个负责多次回调的数据流。
Flow 为什么不允许切换协程?
在讲解 channelFlow() 时有提到,Flow 是不允许切换协程调用 emit(),即有如下操作:
java
fun main() = runBlocking {
val flow = flow {
launch {
delay(2000)
emit(2) // 不允许在子协程切换协程,会抛出异常
}
delay(1000)
emit(1)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
// flow 在这个协程执行
flow.collect {
println("flow: $it")
}
}
delay(10000)
}
我们讨论下为什么 Flow 不允许切换协程来调用 emit()。
假设我们有业务场景:通过 Flow 发送数据,在主线程接收数据并更新 UI,我们可能会有这种处理:
java
fun main() = runBlocking {
val flow = flow {
delay(1000)
emit(1)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch(Dispatchers.Main) {
flow.collect {
// 执行更新 UI 的操作
textView.text = "$it"
}
// flow.collect() 等价于
// delay(1000)
// textView.text = "1"
}
delay(10000)
}
作为开发者我们明确的知道 Flow 收集数据是运行在主线程,所以可以很放心的在 collect() 做更新 UI 的操作。现在在 Flow 切换了协程,这个行为将会与开发者预期不一致,可能就会导致异常:
java
fun main() = runBlocking {
val flow = flow {
launch(Dispatchers.IO) {
delay(2000)
emit(2)
}
delay(1000)
emit(1)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch(Dispatchers.Main) {
flow.collect {
// 执行更新 UI 的操作
textView.text = "$it"
}
// flow.collect() 等价于
// launch {
// delay(2000)
// textView.text = "2" // 子线程更新 UI 抛异常
// }
// delay(1000)
// textView.text = "1"
}
delay(10000)
}
输出结果:
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
Emission from another coroutine is detacted.
...
为了解决上面与开发者预期不一致的问题,有两种方案:
-
告知每个开发者,collect() 调用所在的协程不可靠,要自己调用 collect() 时切到对应协程
-
限制 Flow 切换协程,禁止 emit() 在其他协程发送数据,保证 collect() 一定在所在协程发送
很明显协程选择了第二种方案,也更容易开发者接受不容易被迷惑。
所以为什么 Flow 不允许切换协程调用 emit(),是因为这会导致和开发者在调用 collect() 的预期出现不一致的结果,即预期在这个协程运行,但代码又在另一个协程运行,进而引发各种隐患。
在协程这一点上 Flow 和 channelFlow() 的区别是:channelFlow() 可以切协程是因为用的就是 Channel,Channel 做的事情就是跨协程的,Channel 发送数据,Channel 转换为 Flow 后,调用 flow.collect() 数据的接收还是在 collect() 所在的协程,所以并不会有问题。
Flow 的操作符
filter() 系列操作符
filter()
filter() 操作符可以留下符合条件的数据,过滤不符合条件的数据:
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
// 过滤输出偶数
flow.filter { it % 2 == 0 }.collect { println("$it") }
}
delay(10000)
}
输出结果:
2
4
filterNot()
filterNot() 和 filter() 逻辑相反,filterNot() 留下不符合条件的数据,过滤符合条件的数据:
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
// 过滤输出奇数
flow.filterNot { it % 2 == 0 }.collect { println("$it") }
}
delay(10000)
}
输出结果:
1
3
5
filterNotNull()
filterNotNull() 会把非空的数据留下,过滤掉为 null 的数据:
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, null, 3, null, 4, 5)
scope.launch {
// 过滤为 null 的数据并输出偶数
flow.filterNotNull().filter { it % 2 == 0 }.collect { println("$it") }
}
delay(10000)
}
输出结果:
2
4
filterIsInstance()
filterIsInstance() 是把符合指定类型的元素留下,过滤掉不符合类型的元素:
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, 3, "test", 5)
scope.launch {
// 过滤只输出字符串
flow.filterIsInstance<String>.collect { println("$it") }
// flow.filterIsInstance(String::class).collect { println("$it") }
}
delay(10000)
}
输出结果:
test
filterIsInstance() 有两种写法,第一种是用的关键字 reified 处理的,但这种方式在一些特殊情况会有问题,比如 List:
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, 3, "test", 5, listOf("A", "B"), listOf(1, 2))
scope.launch {
flow.filterIsInstance<List<String>>.collect { println("$it") }
}
delay(10000)
}
输出结果:
[A, B]
[1, 2] // 指定了输出字符串的列表,但还是有输出了 List<Int>
filterIsInstance() 已经指定了过滤输出字符串的 List,但最终 Int 类型的 List 也输出了,原因是 reified 只能解决外层的泛型类型判断,List 内部的类型会被类型擦除无法判断,所以都会输出。
如果我们就需要精确到内部的类型,还是只能用 filter():
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, 3, "test", 5, listOf("A", "B"), listOf(1, 2))
scope.launch {
flow.filter { it is List<*> && it.firstOrNull()?.let { item -> item is String } }.collect { println("$it") }
}
delay(10000)
}
输出结果:
[A, B]
distinctUntilChanged() 和 distinctUntilChangedBy() 操作符
distinctUntilChanged() 和 distinctUntilChangedBy() 两个操作符也是用来过滤的,不过它们不是根据元素自身来过滤,而是用来去重的,即连续发送了两个重复的数据,新的数据就不再发送,间隔非连续的重复数据还是可以接收到。
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, 3, 3, 3, 3, 1)
scope.launch {
// 去除连续重复的数据,有间隔的还是可以接收到
// 内部是用的 kotlin 的 == 即 equals() 判断的
flow.distinctUntilChanged().collect { println("$it") }
}
delay(10000)
}
输出结果:
1
2
3
1
如果想自定义去重逻辑,distinctUntilChanged() 也可以自定义:
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf("test", "Test", "test2")
scope.launch {
// 将元素先转成大写后再去重,也就是忽略大小写的处理
flow.distinctUntilChanged { a, b -> a.uppercase() == b.uppercase() }.collect { println("$it") }
}
delay(10000)
}
输出结果:
test
test2
distinctUntilChangedBy() 可以对元素进行转换后做去重,lambda 是转换为对应的处理后生成 key,不会对原数据有影响:
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf("test", "Test", "test2")
scope.launch {
flow.distinctUntilChangedBy { it.uppercase() }.collect { println("$it") }
}
delay(10000)
}
输出结果:
test
test2
自定义 Flow 操作符
自定义 Flow 操作符其实就是用一个现成的 Flow 对象来创建另一个 Flow 对象,自定义操作符其实就是一个 Flow 的扩展函数。
自定义 Flow 操作符主要有以下步骤:
-
定义 Flow 的扩展函数,提供入参类型和返回值类型(具体类型或泛型)
-
在函数体用 flow() 或 channelFlow() 创建一个空的 Flow 对象
-
在函数体调用 collect() 收集上游的数据
-
在 collect() 内调用 emit(),此时就连接了上游和下游
-
基于上面的基础自定义 Flow 处理发送数据到下游
按上面的步骤我们创建一个啥都不干的自定义 Flow 操作符:
java
fun <T> Flow<T>.customOperator(): Flow<T> = flow {
// 这里的 collect() 是上游 Flow 的 collect()
// 调用的 emit() 是创建的 flow() 提供的
// 这里只连接的上游和下游,其他什么都没变,后续就可以根据这个模板定制 Flow
collect { emit(it) }
}
fun Flow<Int>.double(): Flow<Int> = flow {
collect { emit(it * 2) }
}
fun Flow<Int>.double2(): Flow<Int> = channelFlow {
collect { send(it * 2) }
}
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flowOf(1, 2, 3)
scope.launch {
flow.double().collect { println("$it") }
}
delay(10000)
}
输出结果:
2
4
6
timeout()、sample()、debounce()
timeout()
timeout() 会从 Flow 调用 collect() 开始计时,当超过设定的时间 Flow 还没结束并且没有发出下一条数据,就会抛出 TimeoutCancellationException;如果在设定时间内有收到数据,就会重新开始计时。
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flow {
emit(1)
emit(2)
delay(2000)
emit(3)
}
scope.launch {
try {
flow.timeout(1.seconds).collect { println("$it") }
} catch(e: TimeoutCancellationException) {
// 接收数据超时处理
}
}
delay(10000)
}
输出结果:
1
2
sample()
sample() 会从 Flow 调用 collect() 开始,每隔设定时间内发送过来的数据,只把最新的一条保留接收,其他数据就会被丢弃。适合用在固定时间点刷新的场景,将刷新点之前发送的所有数据只取最新的。
java
fun main() = runBlocking {
val scope = CoroutineScope(CoroutineContext)
val flow = flow {
emit(0)
delay(500)
emit(1) // 在 1s 内的最新数据,保留
delay(800)
emit(2) // 在第二个 1s 内的最新数据,保留
delay(900)
emit(3)
}
scope.launch {
flow.sample(1.seconds).collect { println("$it") }
}
delay(10000)
}
输出结果:
1
2
debounce()
debounce() 会从 Flow 调用 collect() 开始,如果在设定时间内接收到新数据,就会将旧数据丢弃新数据开启新一轮的等待,直到没有新数据超过设定时间了才发送到下游。
debounce() 是不适合做点击事件的去抖动的 ,正常点击事件的响应需要及时的,连续的点击我们就响应第一次,如果用 debounce() 就会出现需要等待到设定时间超时了才响应点击,会出现响应延迟的问题,在用户体验上是比较差的。
如果要做事件点击的去抖动,我们可以自定义 Flow 操作符:
java
fun <T> Flow<T>.throttle(timeWindow: Duration): Flow<T> = flow {
var lastTime = 0L
collect {
if (System.currentTimeMillis() - lastTime > timeWindow.inWholeMilliseconds) {
emit(it)
lastTime = System.currentTimeMillis()
}
}
}
drop()、take() 系列操作符
drop() 和 take() 系列操作符也是过滤功能,但它们特殊性在于是属于截断型过滤,持续丢弃数据直到某条数据之后才开始放行向下转发,或者持续向下转发到达某条数值之后把数据流掐断直接结束数据流,后面的数据全都不要了。
drop()、dropWhile()
drop() 会把你提供的前 n 条数据过滤掉,再往后的数据就开始正常向下转发。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.drop(2).collect { println("$it") }
}
delay(10000)
}
输出结果:
3
4
5
dropWhile() 会让你提供一个判断条件,然后对每个数据都检查,凡事符合这个条件的就把数据丢弃;遇到不符合条件的就把这条数据留下,并且从这条数据开始再往下就不再检查都往下转发。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.dropWhile { it != 3 }.collect { println("$it") }
}
delay(10000)
}
输出结果:
3
4
5
take()、takeWhile()
take() 操作符和 drop() 相反,会让你提供一个数值,然后把数据流的前 n 条数据往下转发,一旦达到提供的数值就掐断直接结束 Flow,后面的数据全都不要。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.take(2).collect { println("$it") }
}
delay(10000)
}
输出结果:
1
2
takeWhile() 和 dropWhile() 相反,会让你提供一个判断条件,在数据符合条件的时候就保持发送,一旦遇到一条不符合的就掐断直接结束 Flow,后面的数据全都不要。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.takeWhile { it != 3 }.collect { println("$it") }
}
delay(10000)
}
输出结果:
1
2
map() 系列操作符
map() 需要让你提供一个算法,把上游过来的数据转换成另一个数据发送到下游。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.map { it + 1 }.collect { println("$it") }
}
delay(10000)
}
输出结果:
2
3
4
5
6
mapNotNull() 是先 map() 转换后的数据如果为空元素就过滤掉,等价于 map() 后 filterNotNull() 的结合。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.mapNotNull { if (it == 3) null else it + 1 }.collect { println("$it") }
// 等价于 flow.map { it + 1 }.filterNotNull().collect {}
}
delay(10000)
}
输出结果:
2
3
5
6
一般的 Flow 操作符是同步的,即发送了数据到下游才开始下一条数据的发送生产,mapLatest() 是异步操作符,会在处理这一条上游数据的过程中,上游依然可以生产下一条数据;如果下一条上游到来了,它会取消正在处理的上一条数据,直接开始处理这条上游的新数据,即 mapLatest() 只关注最新的数据。
mapLatest() 是一个 [有了新数据就停止旧数据的转换流程] 的 map() 的变种。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
delay(100)
emit(1)
delay(100)
emit(2)
delay(100)
emit(3)
}
scope.launch {
flow.mapLatest { delay(120); it + 1 }.collect { println("$it") }
}
delay(10000)
}
输出结果:
4
transform() 系列操作符
transform() 就是更加底层的 map(),同样也是转换后将数据往下游发送,但需要自己手动调用 emit() 发送数据。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2)
scope.launch {
flow.transform {
if (it > 0) {
// 可以在一个操作符发送多条数据
repeat(it) { _ ->
emit("$it - hahaha")
}
}
}.collect { println("$it") }
}
delay(10000)
}
输出结果:
1 - hahaha
2 - hahaha
2 - hahaha
transformWhile() 相当于 transform() 和 takeWhile() 的结合。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.transformWhile {
// 大于 3 就结束 Flow
if (it > 3) return@transformWhile false
if (it > 0) {
repeat(it) { _ ->
emit("$it - hahaha")
}
}
true
}.collect { println("$it") }
}
delay(10000)
}
输出结果:
1 - hahaha
2 - hahaha
2 - hahaha
3 - hahaha
3 - hahaha
3 - hahaha
transformLatest() 就是 mapLatest() 的底层版本。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
delay(100)
emit(1)
delay(100)
emit(2)
delay(100)
emit(3)
}
scope.launch {
flow.transformLatest {
delay(50)
emit("$it - start")
delay(100)
emit("$it - end")
}.collect { println("$it") }
}
delay(10000)
}
输出结果:
1 - start
2 - start
3 - start
3 - end
withIndex() 操作符
withIndex() 会给每个 Flow 发送的元素加上一个序号。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.withIndex().collect { (index, data) ->
println("$index - $data")
}
}
delay(10000)
}
输出结果:
0 - 1
1 - 2
2 - 3
3 - 4
4 - 5
collectIndexed() 也是给元素加上编号,只不过它是在收集的时候加编号,withIndex() 可以在中间流程加编号。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.collectIndexed { index, value ->
println("$index - $value")
}
}
delay(10000)
}
reduce()、fold() 系列操作符
reduce()、runningReduce()
reduce() 就是等差数列的操作符,把所有元素融合到一起来计算一个类型不变的最终的结果;如果只有一个元素不会计算会直接返回。
runningReduce() 是一个运行中的 reduce(),并返回一个新的元素储存对象,能拿到计算过程的每个步骤的结果。
用 List.reduce() 和 List.runningReduce() 计算等差数列求和:
java
val list = listOf(1, 2, 3, 4, 5)
list.reduce { acc, i ->
// acc:累加过程的值
// i:list列表的元素
// 第一轮:acc = 1,i = 2,结果 3
// 第二轮:acc = 3,i = 3,结果 6
// 第三轮:acc = 6,i = 4,结果 10
// 第四轮:acc = 10,i = 5,结果 15
acc + i
}.let { println("List reduced to $it") }
list.runningReduce { acc, i -> acc + i }.let { println("New List: $it") }
输出结果:
List reduced to 15
New List: [1, 3, 6, 10, 15]
Flow 的 reduce() 会在内部调用 collect() 直接启动 Flow 的收集过程,直到所有元素都处理完成并返回最终对应类型的结果;不关心中间过程只关注结果。
Flow 的 runningReduce() 会返回一个新的 Flow 对象,计算过程中每一步的结果都会作为一条数据发送,需要手动 collect() 收集发送的每一步计算后的结果;会参与计算的每个过程。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
// reduce() 会直接启动 Flow 的 collect() 收集,并返回最终对应类型的结果
val sum = flow.reduce { accumulator, value -> accumulator + value }
println("Sum is $sum")
// runningReduce() 会返回一个新的 Flow 对象
flow.runningReduce { accumulator, value -> accumulator + value }
.collect { println("runningReduce: $it") }
}
delay(10000)
}
输出结果:
Sum is 15
runningReduce: 1
runningReduce: 3
runningReduce: 6
runningReduce: 10
runningReduce: 15
fold()、runningFold()
fold() 和 reduce() 的效果是类似的,它们的区别是:
-
fold() 需要提供一个初始值,即使是只有一个元素也会和初始值计算;reduce() 只有一个元素时不会计算会直接返回
-
fold() 提供的初始值类型可以和计算的元素不同,但整个计算过程和返回结果的类型要和初始值的类型一致
java
val list = listOf(1, 2, 3, 4, 5)
list.fold(10) { acc, i -> acc + i }.let { println("List folded to $it") }
list.fold("ha") { acc, i -> "$acc - $i" }.let { println("List folded to string $it") }
list.runningFold("ha") { acc, i -> "$acc - $i" }.let { println("New String List: $it") }
输出结果:
List folded to 25
List folded to string: ha - 1 - 2 - 3 - 4 - 5
New String List: [ha, ha - 1, ha - 1 - 2, ha - 1 - 2 - 3, ha - 1 - 2 - 3 - 4, ha - 1 - 2 - 3 - 4 - 5]
Flow 的 fold() 和 runningFold() 也和列表的一致:
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
val sum = flow.fold("ha") { acc, value -> acc, i -> "$acc - $i" }
println("Flow folded to $it")
flow.runningFold("ha") { acc, value -> acc, i -> "$acc - $i" }
.collect { println("runningFold: $it") }
}
delay(10000)
}
输出结果:
Flow folded to string: ha - 1 - 2 - 3 - 4 - 5
runningFold: ha
runningFold: ha - 1
runningFold: ha - 1 - 2
runningFold: ha - 1 - 2 - 3
runningFold: ha - 1 - 2 - 3 - 4
runningFold: ha - 1 - 2 - 3 - 4 - 5
onEach() 操作符
onEach() 是一个数据监听器作用的操作符,会返回一个新的 Flow 但会把数据原样返回到下游。比如数据从上游发送到下游时经过它可以做一些日志打印的操作。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.onEach {
println("onEach1: $it")
}.filter {
it % 2 == 0
}.onEach {
println("onEach2: $it")
}.collect {
println("collect: $it")
}
}
delay(10000)
}
输出结果:
onEach1: 1
onEach1: 2
onEach2: 2
collect: 2
onEach1: 3
onEach1: 4
onEach2: 4
collect: 4
onEach1: 5
chunked() 操作符
chunked() 需要提供一个分块后元素数量的数值,把 Flow 分块然后输出一个新的、元素类型是 List 的 Flow,每个 List 装载的就是分块后的数据。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
scope.launch {
flow.chunked(2).collect { println("chunked: $it") }
}
delay(10000)
}
输出结果:
chunked: [1, 2]
chunked: [3, 4]
chunked: [5]
catch() 操作符
在实际的项目中我们可能会想着在 Flow 捕获异常,比如下面的代码:
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
// 在 Flow 捕获异常
try {
for (i in 1..5) {
emit(i)
}
} catch (e: Exception) {
println("Error in flow(): $e")
}
}
scope.launch {
// 收集数据的 try-catch 是无效的
// 因为异常都在生产数据的位置被捕获了
try {
flow.collect {
val contributors = gitHub.contributors("square", "retrofit")
println("contributors: $contributors")
}
} catch (e: TimeoutException) {
println("Network error: $e")
}
}
delay(10000)
}
上面的代码在 Flow 生产数据的位置用一个 try-catch 包住,在收集数据的位置也用 try-catch 包住。
但这样捕获异常会出现的问题是,收集数据的 try-catch 是无效的,因为发生异常时,异常已经被生产数据的 try-catch 捕获,并且出现异常时无法正常走协程的异常流程。
Flow 异常处理的原则是:上游的 Flow 不应该吞掉下游的异常,包括 Flow 数据流经过的每一个上游操作符都不能去捕获异常,即上游的生产过程让下游的数据处理过程的异常变得不可见了,这在 Flow 是不允许的。为了保证异常的可见性,[不要在 Flow 用 try/catch] 指的是不要用 try/catch 包住 emit()。
当下游发生异常时,因为下游的所有数据发送都来源于上游的 emit(),Flow 经过的一个个操作符的处理代码都不会执行,而是往上抛异常,直到异常被捕获。
异常捕获官方提供了 catch() 操作符:
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
throw RuntimeException("flow error") // 模拟上游异常被 catch() 捕获
emit(i)
}
}.catch {
// 上游发生异常时才会走到 catch() 操作符捕获异常
println("catch(): $it")
}
scope.launch {
try {
flow.collect {
val contributors = gitHub.contributors("square", "retrofit")
println("contributors: $contributors")
}
} catch (e: TimeoutException) {
println("Network error: $e")
}
}
delay(10000)
}
输出结果:
catch(): java.lang.RuntimeException: flow error
用 catch() 操作符能正常的捕获到上游抛出的异常。
有多个 catch() 操作符时,最靠近下游的 catch() 操作符能捕获它们之间抛出的异常:
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
emit(i)
}
}.catch { println("catch(): $it") }
.onEach { throw RuntimeException("Exception from onEach()") }
.catch { println("catch() 2: $it") }
scope.launch {
try {
flow.collect {
val contributors = gitHub.contributors("square", "retrofit")
println("contributors: $contributors")
}
} catch (e: TimeoutException) {
println("Network error: $e")
}
}
delay(10000)
}
输出结果:
catch() 2: java.lang.RuntimeException: Exception from onEach()
总结下 catch() 操作符的特点:
-
能捕获 Flow 上游抛出的异常,只有一个 catch() 操作符时,catch() 操作符后面的操作符如果出现异常是不能捕获
-
效果类似于用 try-catch 包住 Flow 生产数据的代码块,但又不会捕获 emit() 产生的异常,让异常能到达下游正常走异常流程,符合 [不要用 try/catch 包住 emit()] 的原则
-
不会捕获 CancellationException 异常,保证协程取消流程正常
-
有多个 catch() 操作符时,最靠近下游的 catch() 操作符能捕获它们之间抛出的异常
那么什么时候该用 catch() 操作符,什么时候该用 try-catch?它的作用是什么呢?
catch() 操作符的作用是:当上游发生异常时接管后续数据发送工作的操作符。
用一个例子来说明:
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
if (i == 3) {
throw RuntimeException("flow error")
} else {
emit(i)
}
}
}.catch {
println("catch(): $it")
// 上游发生异常,接管上游继续发送数据给下游
emit(100)
emit(200)
emit(300)
}
scope.launch {
try {
flow.collect {
println("Data: $it")
}
} catch (e: TimeoutException) {
println("Network error: $e")
}
}
delay(10000)
}
输出结果:
Data: 1
Data: 2
catch(): java.lang.RuntimeException: flow error
Data: 100
Data: 200
Data: 300
catch() 操作符就类似于当上游的管道坏了,用一根侧管开始接管后续的数据生产,下游是无感知的。
在使用 Flow 时可能生产数据的代码是别人提供封装好的,我们没法用 try-catch 从内部源码去改动。
catch() 操作符的适用场景:无法修改 Flow 代码从内部通过 try-catch 针对异常修复 Flow 流程的时候,可以用 catch() 操作符做接管。
需要注意的是,[catch() 操作符接管上游数据的发送] 并不是指的完全接管上游的数据发送,而是在上游发生异常时接管做一些收尾的工作告知下游处理。
try-catch 和 catch() 操作符的选择:在能修改 Flow 代码的前提下,发生异常时能用 try-catch 就用 try-catch,但要遵循 try-catch 不能把 emit() 包住;catch() 操作符的接管是一种无法修复 Flow 的无奈之举。
retry()、retryWhen() 操作符
retry()、retryWhen() 和 catch() 操作符一样也是针对上游异常时会触发的操作符,但和 catch() 不同的是,当上游 Flow 异常时 retry()、retryWhen() 可以根据需要选择重启或不重启 Flow。
重启的是整个上游 Flow(包括 retry()、retryWhen() 之前的操作符)的流程,对下游是无感知的;不选择重启则会将异常继续往下抛到下游。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
if (i == 3) {
throw RuntimeException("flow error")
} else {
emit(i)
}
}
}.map { it * 3 }
// 指定重启次数,超过就将异常往下抛
// 指定是否重启的条件,条件返回 true 会重启,返回 false 会直接将异常往下抛
.retry(3) { it is RuntimeException }
// cause:异常的原因
// attempt:已经重试的次数
// .retryWhen { cause, attempt -> }
scope.launch {
try {
flow.collect {
println("Data: $it")
}
} catch (e: RuntimeException) {
println("RuntimeException: $e")
}
}
delay(10000)
}
输出结果:
Data: 2
Data: 4
Data: 2
Data: 4
Data: 2
Data: 4
Data: 2
Data: 4
RuntimeException: java.lang.RuntimeExcepton: flow error
...
onStart()、onCompletion()、onEmpty() 监听流程操作符
onStart()
onStart() 操作符是负责监听 Flow 收集流程的开始事件的,确切的说是 Flow 调用 collect() 后数据生产之前会触发。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
emit(i)
}
}.onStart { println("onStart 1") }
.onStart { println("onStart 2") }
// 如果 onStart() 发生异常只有 catch() 能捕获
.catch { println("catch: $it") }
scope.launch {
flow.collect {
println("Data: $it")
}
}
delay(10000)
}
输出结果:
onStart 2
onStart 1
Data: 1
Data: 2
Data: 3
Data: 4
Data: 5
onCompletion()
onCompletion() 操作符是负责监听 Flow 的结束,即所有数据都发送完结束或异常结束时会触发。异常结束能触发但不会拦截异常。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
if (i == 3) {
throw RuntimeException("flow error")
} else {
emit(i)
}
}
}
// onCompletion() 能拿到异常但不会拦截异常,会继续往下抛被 catch() 捕获到
.onCompletion { println("onCompletion: $it") }
.catch { println("catch: $it") }
scope.launch {
flow.collect {
println("Data: $it")
}
}
delay(10000)
}
输出结果:
Data: 1
Data: 2
onCompletion: java.lang.RuntimeException: flow error
catch: java.lang.RuntimeException: flow error
onEmpty()
onEmpty() 操作符负责监听 Flow 正常结束且没有发送过一条数据的时候被触发,异常结束不会触发。
flowOn() 操作符
flowOn() 操作符是用来定制 Flow 运行的 CoroutineContext,大多数时候是用它来切线程。它只会关注 Flow 的上游,即如果使用 flowOn() 切换线程,那么 flowOn() 上游发送的数据才会在 flowOn() 指定的线程运行,下游还是在启动协程时所在的线程运行。
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
for (i in 1..2) {
emit(i)
}
}.map {
println("CoroutineContext in map() 1: ${currentCoroutineContext()}")
it * 2
}
// 将上游切换到指定线程运行
.flowOn(Dispatchers.IO).map {
// 下游还是在协程启动的线程运行
println("CoroutineContext in map() 2: ${currentCoroutineContext()}")
it * 2
}
scope.launch {
flow.collect {
println("Data: $it - ${currentCoroutineContext()}")
}
}
delay(10000)
}
输出结果:
// 注意:打印输出的结果并不一定是按操作符编写的顺序打印的,这与 Flow 的缓冲有关,会在讲 buffer() 时说明
CoroutineContext in flow(): [ProducerCoroutine{Active}@46553461, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@46553461, Dispatchers.IO]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@1a6e1aba, Dispatchers.Default]
Data: 1 - [ScopeCoroutine{Active}@1a6e1aba, Dispatchers.Default]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@46553461, Dispatchers.IO]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@1a6e1aba, Dispatchers.Default]
Data: 2 - [ScopeCoroutine{Active}@1a6e1aba, Dispatchers.Default]
当我们尝试连续使用 flowOn() 时,Flow 会出现 [融合] 的情况,即两个 flowOn() 会共用同一个 Flow 对象,但它们的 CoroutineContext 会合并到一起(CoroutineContext 类型不同时合并,类型相同时只保留一个),flowOn() 右边的 CoroutineContext 加到左边 flowOn() 的 CoroutineContext:
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
for (i in 1..2) {
emit(i)
}
}
// 两个 flowOn() 会共用一个 Flow 对象
// 然后 CoroutineContext 会是 CoroutineName + Dispatchers.IO
// 右边的 flowOn() 的 CoroutineContext 加到左边 flowOn() 的 CoroutineContext
.flowOn(Dispatchers.IO).flowOn(CoroutineName("flowOn"))
scope.launch {
flow.collect {
println("Data: $it - ${currentCoroutineContext()}")
}
}
delay(10000)
}
输出结果:
CoroutineContext in flow(): [CoroutineName(flowOn), ProducerCoroutine{Active}@46553461, Dispatchers.IO]
Data: 1 - [ScopeCoroutine{Active}@1a6e1aba, Dispatchers.Default]
Data: 2 - [ScopeCoroutine{Active}@1a6e1aba, Dispatchers.Default]
**flowOn() 为什么只切换上游的 CoroutineContext?**原因也很简单,使用 Flow 是为了将数据的生产和数据的收集拆分开,那就有可能由多个开发人员负责,比如一个负责开发生产数据的流程,一个负责开发收集数据的流程,如果 flowOn() 把上下游的 CoroutineContext 都切换了,就会导致下游的代码行为变得难以预期,开发收集数据流程的开发以为我的程序会在这个协程的 CoroutineContext 运行,实际上被上游切换了。
withContext() 和 flowOn() 都可以切换线程,二者的选择主要考虑切换线程的颗粒度:
-
如果只想在某一个操作符切换线程,那可以用 withContext()
-
如果针对的是整个 Flow 上游生产流程或多个 Flow 操作符切换线程,用 flowOn() 会比较方便,因为不需要对每个操作符都用 withContext() 切换线程
flowOn() 只管上游的线程切换,不做其他处理的情况下,下游会在启动协程所在的线程运行;
如果我们想下游不在启动协程所在的线程运行,有两种官方推荐的实现方案:
-
在 onEach() 实现具体收集数据的逻辑,flowOn() 切换线程,collect() 空实现
-
在 onEach() 实现具体收集数据的逻辑,launchIn() 切换线程
java
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
for (i in 1..2) {
emit(i)
}
}.flowOn(Dispatchers.IO)
scope.launch {
flow.onEach {
// collect() 空实现,在 onEach() 实现具体收集数据的逻辑
// 这样就能让下游的线程也交给 flowOn() 管理
println("Data: $it - ${currentCoroutineContext()}")
}.flowOn(newFixedThreadPoolContext(2, "TestPool")).collect {}
// flow.onEach {
// println("Data: $it - ${currentCoroutineContext()}")
// }.launchIn(scope + Dispatchers.IO)
}
delay(10000)
}
总结
1、Flow 的创建
日常创建 Flow 的方式有 flow()、flowOf(),将其他数据结构转换为 Flow 是 asFlow()。
我们还提到了 Channel 转换为 Flow 的方式 consumeAsFlow()、receiveAsFlow()、channelFlow() 和 callbackFlow()。
(1)consumeAsFlow() 和 receiveAsFlow()
consumeAsFlow() 和 receiveAsFlow() 的区别:
-
consumeAsFlow() 只能被消费一次,调用多次 collect() 会抛出异常
-
receiveAsFlow() 多次接收不会抛异常
(2)channelFlow()
channelFlow() 与 consumeAsFlow() 和 receiveAsFlow() 的区别:
-
consumeAsFlow() 和 receiveAsFlow() 是用一个现成的 Channel 作为数据源,所有 collect() 来共享消费瓜分 Channel 发送的数据
-
channelFlow() 是直到调用 collect() 才会创建 Channel,多次调用 collect() 就会创建多个 Channel,这些 Channel 的生产流程是互相隔离、各自独立的
channelFlow 的使用场景:
-
需要在 Flow 启动子协程
-
Flow 是不允许切换协程调用 emit(),而你有跨协程生产数据的需求
(3)callbackFlow()
callbackFlow() 在使用上和 channelFlow() 是完全一样的。它主要是将回调 API 转换成 Flow 处在协程环境。
suspendCancellationCoroutine() 与 callbackFlow() 的区别是:
-
suspendCancellationCoroutine() 能处理单次的回调切换到协程环境
-
callbackFlow() 可以处理连续的回调
callbackFlow() 可以看成是 Flow 版的 suspendCancellationCoroutine(),一个负责单次的回调,一个负责多次回调的数据流。
2、Flow 为什么不允许切换协程?
Flow 不允许切换协程调用 emit(),是因为这会导致和开发者在调用 collect() 的预期出现不一致的结果,即预期在这个协程运行,但代码又在另一个协程运行,进而引发各种隐患。
在协程切换这一点上 Flow 和 channelFlow() 的区别是:channelFlow() 可以切协程是因为用的就是 Channel,Channel 做的事情就是跨协程的,Channel 发送数据,Channel 转换为 Flow 后,调用 flow.collect() 数据的接收还是在 collect() 所在的协程,所以并不会有问题。
3、Flow 的操作符
(1)filter() 系列操作符
-
filter():留下符合条件的数据,过滤不符合条件的数据
-
filterNot() 留下不符合条件的数据,过滤符合条件的数据
-
filterNotNull() 会把非空的数据留下,过滤掉为 null 的数据
-
filterIsInstance() 是把符合指定类型的元素留下,过滤掉不符合类型的元素
(2)distinctUntilChanged() 和 distinctUntilChangedBy() 操作符
distinctUntilChanged() 和 distinctUntilChangedBy() 两个操作符也是用来过滤的,不过它们不是根据元素自身来过滤,而是用来去重的,即连续发送了两个重复的数据,新的数据就不再发送,间隔非连续的重复数据还是可以接收到。
(3)自定义 Flow 操作符
自定义 Flow 操作符其实就是用一个现成的 Flow 对象来创建另一个 Flow 对象,自定义操作符其实就是一个 Flow 的扩展函数。
自定义 Flow 操作符主要有以下步骤:
-
定义 Flow 的扩展函数,提供入参类型和返回值类型(具体类型或泛型)
-
在函数体用 flow() 或 channelFlow() 创建一个空的 Flow 对象
-
在函数体调用 collect() 收集上游的数据
-
在 collect() 内调用 emit(),此时就连接了上游和下游
-
基于上面的基础自定义 Flow 处理发送数据到下游
(4)timeout()、sample()、debounce()
-
timeout():从 Flow 调用 collect() 开始计时,当超过设定的时间 Flow 还没结束并且没有发出下一条数据,就会抛出 TimeoutCancellationException;如果在设定时间内有收到数据,就会重新开始计时
-
sample() :从 Flow 调用 collect() 开始,每隔设定时间内发送过来的数据,只把最新的一条保留接收,其他数据就会被丢弃。适合用在固定时间点刷新的场景,将刷新点之前发送的所有数据只取最新的
-
debounce():从 Flow 调用 collect() 开始,如果在设定时间内接收到新数据,就会将旧数据丢弃新数据开启新一轮的等待,直到没有新数据超过设定时间了才发送到下游
(5)drop()、take() 系列操作符
-
drop():会把你提供的前 n 条数据过滤掉,再往后的数据就开始正常向下转发
-
dropWhile():会让你提供一个判断条件,然后对每个数据都检查,凡事符合这个条件的就把数据丢弃;遇到不符合条件的就把这条数据留下,并且从这条数据开始再往下就不再检查都往下转发
-
take():和 drop() 相反,会让你提供一个数值,然后把数据流的前 n 条数据往下转发,一旦达到提供的数值就掐断直接结束 Flow,后面的数据全都不要
-
takeWhile():和 dropWhile() 相反,会让你提供一个判断条件,在数据符合条件的时候就保持发送,一旦遇到一条不符合的就掐断直接结束 Flow,后面的数据全都不要
(6)map() 系列操作符
-
map():需要让你提供一个算法,把上游过来的数据转换成另一个数据发送到下游
-
mapNotNull():先 map() 转换后的数据如果为空元素就过滤掉;等价于 map() 后 filterNotNull() 的结合
-
mapLatest():它是异步操作符,会在处理这一条上游数据的过程中,上游依然可以生产下一条数据;如果下一条上游到来了,它会取消正在处理的上一条数据,直接开始处理这条上游的新数据,即 mapLatest() 只关注最新的数据
(7)transform() 系列操作符
-
transform():就是更加底层的 map(),同样也是转换后将数据往下游发送,但需要自己手动调用 emit() 发送数据
-
transformWhile():相当于 transform() 和 takeWhile() 的结合
-
transformLatest():mapLatest() 的底层版本
(8)withIndex() 操作符
-
withIndex():会给每个 Flow 发送的元素加上一个序号
-
collectIndexed():也是给元素加上编号,只不过它是在收集的时候加编号,withIndex() 可以在中间流程加编号
(9)reduce()、fold() 系列操作符
reduce() 和 fold() 操作符可以通过 Iterable 的类比 Flow 同名操作符的功能。
-
reduce():等差数列操作符,把所有元素融合到一起来计算一个类型不变的最终的结果;如果只有一个元素不会计算会直接返回
-
runningReduce():运行中的 reduce(),并返回一个新的元素储存对象,能拿到计算过程的每个步骤的结果
Flow 的 reduce() 会在内部调用 collect() 直接启动 Flow 的收集过程,直到所有元素都处理完成并返回最终对应类型的结果;不关心中间过程只关注结果。
Flow 的 runningReduce() 会返回一个新的 Flow 对象,计算过程中每一步的结果都会作为一条数据发送,需要手动 collect() 收集发送的每一步计算后的结果;会参与计算的每个过程。
fold() 和 reduce() 的效果是类似的,它们的区别是:
-
fold() 需要提供一个初始值,即使是只有一个元素也会和初始值计算;reduce() 只有一个元素时不会计算会直接返回
-
fold() 提供的初始值类型可以和计算的元素不同,但整个计算过程和返回结果的类型要和初始值的类型一致
(10)onEach() 操作符
onEach() 是一个数据监听器作用的操作符,会返回一个新的 Flow 但会把数据原样返回到下游。比如数据从上游发送到下游时经过它可以做一些日志打印的操作。
(11)chunked() 操作符
chunked() 需要提供一个分块后元素数量的数值,把 Flow 分块然后输出一个新的、元素类型是 List 的 Flow,每个 List 装载的就是分块后的数据。
(12)catch() 操作符
catch() 操作符的特点:
-
能捕获 Flow 上游抛出的异常,只有一个 catch() 操作符时,catch() 操作符后面的操作符如果出现异常是不能捕获
-
效果类似于用 try-catch 包住 Flow 生产数据的代码块,但又不会捕获 emit() 产生的异常,让异常能到达下游正常走异常流程,符合 [不要用 try/catch 包住 emit()] 的原则
-
不会捕获 CancellationException 异常,保证协程取消流程正常
-
有多个 catch() 操作符时,最靠近下游的 catch() 操作符能捕获它们之间抛出的异常
catch() 操作符的作用是:当上游发生异常时接管后续数据发送工作的操作符。
catch() 操作符的适用场景:无法修改 Flow 代码从内部通过 try-catch 针对异常修复 Flow 流程的时候,可以用 catch() 操作符做接管。
需要注意的是,[catch() 操作符接管上游数据的发送] 并不是指的完全接管上游的数据发送,而是在上游发生异常时接管做一些收尾的工作告知下游处理。
try-catch 和 catch() 操作符的选择:在能修改 Flow 代码的前提下,发生异常时能用 try-catch 就用 try-catch,但要遵循 try-catch 不能把 emit() 包住;catch() 操作符的接管是一种无法修复 Flow 的无奈之举。
(13)retry()、retryWhen() 操作符
retry()、retryWhen() 和 catch() 操作符一样也是针对上游异常时会触发的操作符,但和 catch() 不同的是,当上游 Flow 异常时 retry()、retryWhen() 可以根据需要选择重启或不重启 Flow。
重启的是整个上游 Flow(包括 retry()、retryWhen() 之前的操作符)的流程,对下游是无感知的;不选择重启则会将异常继续往下抛到下游。
(14)onStart()、onCompletion()、onEmpty() 监听流程操作符
-
onStart():负责监听 Flow 收集流程的开始事件的,确切的说是 Flow 调用 collect() 后数据生产之前会触发
-
onCompletion():负责监听 Flow 的结束,即所有数据都发送完结束或异常结束时会触发。异常结束能触发但不会拦截异常
-
onEmpty():负责监听 Flow 正常结束且没有发送过一条数据的时候被触发,异常结束不会触发
(15)flowOn() 操作符
flowOn() 操作符是用来定制 Flow 运行的 CoroutineContext,大多数时候是用它来切线程。它只会关注 Flow 的上游,即如果使用 flowOn() 切换线程,那么 flowOn() 上游发送的数据才会在 flowOn() 指定的线程运行,下游还是在启动协程时所在的线程运行。
flowOn() 限制只切换上游的 CoroutineContext 的原因也很简单,如果 flowOn() 把上下游的 CoroutineContext 都切换了,就会导致下游的代码行为变得难以预期。
withContext() 和 flowOn() 都可以切换线程,二者的选择主要考虑切换线程的颗粒度:
-
如果只想在某一个操作符切换线程,那可以用 withContext()
-
如果针对的是整个 Flow 上游生产流程或多个 Flow 操作符切换线程,用 flowOn() 会比较方便,因为不需要对每个操作符都用 withContext() 切换线程
如果我们想下游不在启动协程所在的线程运行,有两种官方推荐的实现方案:
-
在 onEach() 实现具体收集数据的逻辑,flowOn() 切换线程,collect() 空实现
-
在 onEach() 实现具体收集数据的逻辑,launchIn() 切换线程