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

相关推荐
七月.末1 小时前
安卓aab包的安装教程,附带adb环境的配置
android·adb
SRC_BLUE_176 小时前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道9 小时前
Android打包流程图
android
镭封11 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰48711 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛11 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二11 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫12 小时前
Android开发中的隐藏控件技巧
android
Winston Wood13 小时前
Android中Activity启动的模式
android
众乐认证13 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto