开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情
1.认识Flow上游
先看一下Flow的简单使用
fun flowTest() = runBlocking {
flow {
emit(0)
emit(1)
emit(2)
emit(3)
emit(4)
}.collect {
println("it:$it")
}
}
//输出结果:
//it:0
//it:1
//it:2
//it:3
//it:4
Flow从字面意思理解就是流,Flow除了有发送方和接收方之外还可以有中转站,什么是中转站呢,例如水流,水流从源头汇入大海中间会经过水库、支流等。
Flow的中转站用法
fun flowTest() = runBlocking {
flow {
emit(0)
emit(1)
emit(2)
emit(3)
emit(4)
}.filter { //中转站①
it > 2
}.map { //中转站②
it * 2
}.collect { //接收
println("it:$it")
}
}
//输出结果:
//it:6
//it:8
对上面的代码进行逐个分析:
- flow{}: 是一个高阶函数,作用就是创建一个新的
Flow,创建好后就要把消息发送出去,这里的emit是发射、发送的意思,那么flow{}的作用就是创建一个数据流并且将数据发送出去; - filter{}、map{}: 这是中间操作符,都是高阶函数,就像中转站一样对数据进行处理后向下传递;
- collect{}: 终止操作符,终止
Flow数据流并接收从上游传递的数据。
除了通过flow{}创建Flow之外还有flowOf{},也可以创建一个Flow
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
it > 2
}.map {
it * 2
}.collect {
println("it:$it")
}
}
//输出结果:
//it:6
//it:8
上面通过两段代码实现了Flow的创建,这时候两有个疑问:
1.collect是终止操作符,作用是接收从上游传递的数据,那要是不接收会怎么样?
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
println("filter:$it")
it > 2
}
.map {
println("map:$it")
it * 2
}
}
//没有输出任何日志程序就结束了
运行上面的代码会发现什么都没有做就结束了,而添加collect函数后filter和map的日志就是正常输出的,因此得出一个结论:只有调用终止操作符collect之后,Flow 才会开始工作。
已知Channel是“热”的,它不管有没有接收方,发送方都会工作, 那么可以总结出Flow就是冷的,Flow没有接收方是不会开始工作的。
2.两段代码都发送了5条数据,然后由collect接收,那么是一次发送完毕还是逐条发送呢?
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
println("filter:$it")
it > 2
}.map {
println("map:$it")
it * 2
}.collect {
println("it:$it")
}
}
//输出结果:
//filter:0
//filter:1
//filter:2
//filter:3
//map:3
//it:6
//filter:4
//map:4
//it:8
从输出结果可以很清楚的知道Flow一次只会处理一条数据。
上面是通过Flow的API创建一个流,但是还有一个更神奇的方式也可以实现近似于flowOf的效果
fun flowTest() = runBlocking {
//区别在这里
// ↓
listOf(0, 1, 2, 3, 4)
.filter {
it > 2
}.map {
it * 2
}.forEach() { //区别在这里
println("it:$it")
}
}
//输出结果:
//it:6
//it:8
flowOf和listOf的代码对比之后可以看到 Flow API 与集合 API 之间的共性。listOf 创建 List,遍历使用 forEach{};flowOf 创建 Flow,遍历 Flow使用 collect{}。
Kotlin还提供了Flow转List、List转Flow的API
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.toList() //Flow转List
.filter {
it > 2
}.map {
it * 2
}.forEach { //collect变为forEach
println("it:$it")
}
listOf(0, 1, 2, 3, 4)
.asFlow() //List转
.filter {
it > 2
}.map {
it * 2
}.collect { //forEach变为collect
println("it:$it")
}
}
//输出结果:
//it:6
//it:8
//it:6
//it:8
flow、flowOf、listOf否尅创建Flow,那么他们有什么区别?
| Flow创建方式 | 使用场景 | 用法 |
|---|---|---|
| flow | 未知数据集 | flow{ emit() }.collect{ } |
| flowOf | 已知数据集 | flowOf(T).collect{ } |
| listOf | 已知数据来源的集合 | listOf(T).asFlow().collect{ } |
2.认识Flow中转站
Flow中转站指的就是中间操作符:
1.与集合一样的操作符,这里只是摘抄了部分操作符
/**
* 返回只包含与给定[predicate]匹配的原始流的值的流
*/
public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
if (predicate(value)) return@transform emit(value)
}
/**
* 返回只包含与给定[predicate]值不匹配的原始流的值的流
*/
public inline fun <T> Flow<T>.filterNot(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
if (!predicate(value)) return@transform emit(value)
}
/**
* 返回一个只包含原始流的非空值的流
*/
public fun <T: Any> Flow<T?>.filterNotNull(): Flow<T> = transform<T?, T> { value ->
if (value != null) return@transform emit(value)
}
/**
* 返回一个流,其中包含对原始流的每个值应用给定[transform]函数的结果。
*/
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
return@transform emit(transform(value))
}
/**
* 返回一个流,将每个元素包装成[IndexedValue],包含value和它的索引(从0开始)。
*/
public fun <T> Flow<T>.withIndex(): Flow<IndexedValue<T>> = flow {
var index = 0
collect { value ->
emit(IndexedValue(checkIndexOverflow(index++), value))
}
}
/**
* 返回一个流,在上游流的每个值被下游发出之前调用给定的[action]。
*/
public fun <T> Flow<T>.onEach(action: suspend (T) -> Unit): Flow<T> = transform { value ->
action(value)
return@transform emit(value)
}
2.Flow特有的操作符——Flow生命周期
/**
* 返回在开始收集此流之前调用给定操作的流。
* 该操作在上游流启动之前被调用,因此如果它与SharedFlow一起使用,
* 则不能保证上游流内部或onStart操作之后发生的排放会被收集
*/
public fun <T> Flow<T>.onStart(
action: suspend FlowCollector<T>.() -> Unit
): Flow<T> = unsafeFlow {
...
}
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
println("filter")
it > 2
}.map {
println("map")
it * 2
}.onStart {
println("onStart")
}.collect {
println("it:$it")
}
}
//输出结果:
//onStart
//filter
//filter
//filter
//filter
//map
//it:6
//filter
//map
//it:8
可以看到onStart函数的执行数序与它在代码中定义的顺序没有关系,而其他两个操作符filter、map的执行流程则跟它们定义的顺序息息相关。
/**
* 返回一个流,该流在流完成或取消后调用给定的操作,传递取消异常或失败作为操作的原因参数。
* 从概念上讲,onCompletion类似于将流集合包装到finally块中,例如下面的命令代码片段:
*/
public fun <T> Flow<T>.onCompletion(
action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit
): Flow<T> = unsafeFlow {
...
}
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
println("filter")
it > 2
}.map {
println("map")
it * 2
}.onCompletion {
println("onCompletion")
}.collect {
println("it:$it")
}
}
//输出结果:
//filter
//filter
//filter
//filter
//map
//it:6
//filter
//map
//it:8
//onCompletion
onCompletion在它的注释中也标注的比较清楚,类似于finally,都是在最后执行。
3.Flow特有的操作符——catch异常处理
Flow中的catch异常处理时要遵循上下游规则的,因为Flow是具有上下游之分的,具体来讲就是catch只能管理自己上游发生的异常,对于它下游的异常则无能为力,用代码来展示一下他们的区别:
- 上游发生异常,在异常后捕获
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
println("filter")
it > 2
}.map {
println("map")
it * 2
}.map {
it / 0
}.catch {
println("catch:$it")
}.collect {
println("it:$it")
}
}
//输出结果:
//filter
//filter
//filter
//filter
//map
//catch:java.lang.ArithmeticException: / by zero
- 上游捕获异常,下游发生异常
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
println("filter")
it > 2
}.map {
println("map")
it * 2
}.catch {
println("catch:$it")
}.map {
it / 0
}.collect {
println("it:$it")
}
}
//输出结果:
//filter
//filter
//filter
//filter
//map
java.lang.ArithmeticException: / by zero
从两段代码可以非常清楚的总结出:上游发生异常并在异常后捕获是不会造成程序终止的,而在上游捕获异常下游发生异常时则会造成程序终止。
那么下游的异常就无法捕获了吗?并不是,对于下游的异常可以考虑采用最传统的做法try catch
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
println("filter")
it > 2
}.map {
println("map")
it * 2
}.catch {
println("catch:$it")
}.map {
try {
it / 0
} catch (e: Exception) {
println("catch:${e.message}")
}
}.collect {
println("it:$it")
}
}
//输出结果:
//filter
//filter
//filter
//filter
//map
//catch:/ by zero
//it:kotlin.Unit
//filter
//map
//catch:/ by zero
//it:kotlin.Unit
catch执行两次主要是因为前面的操作符返回的结果。
所以一句话总结就是:Flow中的catch操作符的作用与它所在的位置是强相关的,catch无法捕获的可以采用try catch捕获。
4.Flow特有的操作符——切换Context:flowOn、launchIn
Flow因为它具有上游、中间操作符、下游的特性,使得它可以处理复杂且异步执行的任务,那么异步执行的任务中大多又涉及到线程切换,Flow也恰好提供了线程切换的API。
- flowOn
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
logX("filter:$it")
it > 2
}
.flowOn(Dispatchers.IO) //变化在这里
.map {
logX("map:$it")
it * 2
}
.collect {
logX("it:$it")
}
}
//输出结果:
//================================
//filter:0
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:1
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:2
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:3
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//filter:4
//Thread:DefaultDispatcher-worker-1
//================================
//================================
//map:3
//Thread:main
//================================
//================================
//it:6
//Thread:main
//================================
//================================
//map:4
//Thread:main
//================================
//================================
//it:8
//Thread:main
//================================
flowOn线程的切换范围与catch一样仅针对上游,那么要制定collect中的Context该怎么办?可以使用withContext,但是如果除了collect之外还想让其他操作符也运行在collect所在的线程中就会遇到问题,虽然依旧可以使用withContext但是这样的写法就会很丑陋,就像下面这样失去了原本简洁的链式调用。那么解决这个问题的另一种方案launchIn就派上用场了。
fun flowTest() = runBlocking {
withContext(myDispatcher) {
flowOf(0, 1, 2, 3, 4)
.filter {
logX("filter:$it")
it > 2
}
.flowOn(Dispatchers.IO)
.map {
logX("map:$it")
it * 2
}
.collect {
logX("it:$it")
}
}
}
- launchIn
fun flowTest() = runBlocking {
flowOf(0, 1, 2, 3, 4)
.filter {
logX("filter:$it")
it > 2
}
.flowOn(Dispatchers.IO)
.map {
logX("map:$it")
it * 2
}
.onEach { //onEach实现类似 collect{} 的功能
logX("onEach:$it")
}
.launchIn(CoroutineScope(myFlowDispatcher))
}
//输出结果:
//map{}、onEach{}、flow{}运行在myFlowDispatcher
//filter{}运行在DefaultDispatcher
//launchIn源码
/**
* 在[作用域]中[启动][启动]给定流的[集合][收集]的终端流操作符。
* 它是‘scope’的缩写。启动{flow.collect()} '。
* 该操作符通常与[onEach]、[onCompletion]和[catch]操作符一起使用,
* 处理所有发出的值,处理可能在上游流或处理过程中发生的异常,
*/
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect()
}
使用了launchIn操作符的flow无法再调用collect,从launchIn源码中可知,launchIn调用了collect()。
3.Flow下游——终止操作符
在Flow中最常见的操作符就是collect,除此之外还有first()、single()、fold{}、reduce{}。这几个操作符
/**
* 终端操作符,它返回流发出的第一个元素,然后取消流的集合
*/
public suspend fun <T> Flow<T>.first(): T {}
/**
* 等待且仅等待一个值发出的终端操作符。对空流抛出NoSuchElementException,
* 对包含多个元素的流抛出IllegalStateException。
*/
public suspend fun <T> Flow<T>.single(): T {}
/**
* 从初始值开始累加值,并应用操作累加器、累加值和每个元素
*/
public suspend inline fun <T, R> Flow<T>.fold(
initial: R,
crossinline operation: suspend (acc: R, value: T) -> R
): R {}
/**
* 从第一个元素开始累加值,并对当前累加器值和每个元素应用操作。如果流为空,则抛出NoSuchElementException。
*/
public suspend fun <S, T : S> Flow<T>.reduce(operation: suspend (accumulator: S, value: T) -> S): S {}
另外,当Flow调用toList转换成集合后toList后面的API都不再属于Flow因此这也就说明toList也算是一种终止操作符。
4.Flow的特点
- Flow是冷的,只有接收方存在才会工作;
- Flow是懒得,一次只发送一条数据。