4.4-Channel 和 Flow:Flow 的创建、收集和操作符

文章目录

  • [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 的操作符)
  • 总结

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() 切换线程

相关推荐
拭心3 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王6 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡6 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道6 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库7 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道8 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe8 小时前
Android Hook - 动态加载so库
android
居居飒8 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He11 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗12 小时前
Android笔试面试题AI答之Android基础(1)
android