前言:
在上一篇文章中,我们介绍了流的基础知识。包括流的创建方式,冷流和热流的区别,以及流是否是粘性的。感兴趣的读者可以去看看这篇文章:
第一节:Flow的基础知识
本篇文章我们主要来介绍下流的一些常见基础操作符的使用。
1.遍历操作符onEach
fun main() {
runBlocking {
listOf(3, 1, 2)
.asFlow()
.onEach { item -> println("item = $item") }
.collect()
}
}
// 输出
item = 1
item = 2
item = 3
onEach操作符是flow中一个比较基础操作符,简单来说就是遍历流中的每一个元素。当我们需要查看流中每个数据的状态时onEach操作符很有用。
2.过滤操作符filter
顾名思义,filter操作符,是用来过滤满足我们条件的数据,如下代码示例:
fun main() {
runBlocking {
listOf(1, 2, 3, 4, 5)
.asFlow()
.filter { it > 2 }
.collect {
println("collect result = $it")
}
}
}
// 输出
collect result = 3
collect result = 4
collect result = 5
从输出结果可以看出,collect函数中只接受满足条件大于2的数据。
3.转换操作符transform
transform操作符,允许我们将现有流中的数据类型转换成另一种数据类型。但是这种转换后的数据并没有进行emit()操作,所以我们需要自己手动emit()一次数据,这将和我们下面所要介绍的map操作符有一定的关联,具体示例如下:
fun main() {
runBlocking {
flowOf(1, 2, 3)
.transform<Int, String> { emit("transform to string: $it") }
.collect { value ->
println("value: $value")
}
}
}
// 输出
value: transform to string: 1
value: transform to string: 2
value: transform to string: 3
从输出结果来看,我们的flow中原始的数据类型是个Int,经过transform操作符转换后,最终的数据类型变成了String。而且在transfrom操作符的lambda表达式中需要我们自己手动emit(),因为transform操作符内部仅仅是使用Lambda表达式进行了一次数据转换,而并没有帮我们做emit()的动作。下面我们来看下transform操作符的具体实现:
public inline fun <T, R> Flow<T>.transform(
@BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R> = flow { // Note: safe flow is used here, because collector is exposed to transform on each operation
// 这里可能不太好理解,这里其实在收集Flow<T>,将flowOf中发送的T,在这里收集,
// 将T作为参数传递给transform,这样我们就可以在transform操作符的Lambda中拿到这个T,
// 然后再调用处再将T -> R 转换为R。
collect { value ->
// kludge, without it Unit will be returned and TCE won't kick in, KT-28938
return@collect transform(value)
}
}
由上述代码我们可以看出tansform操作符是一个顶层、内联、泛型、扩展函数,我们的原始的flow是
Flow<T> 返回 -> Flow<R>
在transform()函数中还有一个挂起、扩展函数类型的参数。通过Lambda将数据类型中的T转换成R。
4.转换操作符map
事实上我们在上述transform操作符中做了emit()操作就是相当于调用了map操作符,因为map操作符其实是通过transform操作符来实现的。
fun main() {
runBlocking {
flowOf(1, 2, 3)
.map { "map to string: $it" }
.collect { value ->
println("value: $value")
}
}
}
// 输出
value: map to string: 1
value: map to string: 2
value: map to string: 3
map操作符的源码:
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
return@transform emit(transform(value))
}
通过源码我们可以看到map操作符就是将transform操作符中Lambda转换后的数据类型做了一次emit(transform(value))操作。
5.防抖操作符debouce
debouce操作符对于我们处理短时间内发送多条数据的防抖很有用,它可以在我们指定的时间内才响应数据的收集。
fun main() {
runBlocking {
flow{
emit(100)
delay(500)
emit(100)
delay(500)
emit(100)
}
.debounce(1000)
.collect { value ->
println("value: $value")
}
}
}
// 输出
value: 100
由这段代码我们可以看到,我们在flow的Lambda中一共发送了3条数据,前两条数据每次发送以后都会delay500毫秒在发送,但只有最后一条数据被收集到了。在实际开发中,如果我们想做点击事件防抖,就可以使用debouce操作符进行封装。
6.采样操作符sample
和debouce操作符有些类似,sample操作符也是接受一个Long类型的时间参数,sample
/ˈsæmpl/ 翻译成中文是采样的意思。通常用来处理短时间内有大量数据发送时,而我们又没有必要去展示所有的数据,比如屏幕发弹幕(这里是参照郭霖老师的博客)。
fun main() {
runBlocking {
flow{
while(true) {
emit("用户发了一条消息!")
}
}
.flowOn(Dispatchers.IO) // 不切个线程跑起来就卡主了
.sample(1000)
.collect { value ->
println("value: $value")
}
}
}
// 输出
value: 用户发了一条消息!
value: 用户发了一条消息!
value: 用户发了一条消息!
...
由输出结果我们可以看到们在每隔一秒中会采集一条数据发在弹幕上,上面我们用了flowOn操作符做了一次线程切换,main函数默认是运行在主线程的。
fun main() {
runBlocking {
flow{
emit("用户发了一条消息!${Thread.currentThread().name}")
}
.collect { value ->
println("value: $value")
}
}
}
// 输出
value: 用户发了一条消息!main
7.末端操作符reduce
为什么说reduce是末端操作符呢?因为通常末端操作符在Lambda表示式内部会帮我们自行处理collect操作,返回值也通常是Lambda的计算结果而不是一个flow。下面我们先来看下reduce操作符的源码:
public suspend fun <S, T : S> Flow<T>.reduce(operation: suspend (accumulator: S, value: T) -> S): S {
var accumulator: Any? = NULL
collect { value ->
accumulator = if (accumulator !== NULL) {
@Suppress("UNCHECKED_CAST")
operation(accumulator as S, value)
} else {
value
}
}
if (accumulator === NULL) throw NoSuchElementException("Empty flow can't be reduced")
@Suppress("UNCHECKED_CAST")
return accumulator as S
}
由reduce操作符的实现源码我们可以看出,reduce函数中如果只收集到一条数据(当accumulator变量为null时),那么就会返回数据本身,如果大于一条数据(当accumulator变量不为null时),就返回opeartion计算的结果值。具体示例代码如下:
fun main() {
runBlocking {
val accumulator = flowOf(1, 2, 3)
.reduce { accumulator, value -> accumulator + value }
println("result = $accumulator")
}
}
// 输出
result = 6
8.末端操作符fold
fold操作符和reduce操作符很类似,区别在于fold操作符有两个参数,第一个参数是初始值,第二个参数是一个函数类型的参数。其实现过程和reduce很类似,具体源码如下:
public suspend inline fun <T, R> Flow<T>.fold(
initial: R,
crossinline operation: suspend (acc: R, value: T) -> R
): R {
var accumulator = initial
collect { value ->
accumulator = operation(accumulator, value)
}
return accumulator
}
示列代码如下:
fun main() {
runBlocking {
val accumulator = flowOf(1, 2, 3)
.fold(10) { accumulator, value -> accumulator + value }
println("result = $accumulator")
}
}
// 输出
result = 16
9.异常操作符catch
catch操作符是用来捕获流中的异常,但该操作符有个缺陷,就是它只能捕获到在它上游的流发生的异常,并不能捕获其下游的异常。下面我们来看一个具体的示例:
fun main() {
runBlocking {
flow {
emit(1)
throw Exception("I am catch.")
}.catch {
println("catch: $it")
}
.collect {
println("collect: $it")
}
}
}
// 输出
collect: 1
catch: java.lang.Exception: I am catch.
从输出结果来看,在我们发送一条数据后,catch操作符成功的捕获到了我们抛出的异常,但是如果我们在catch的下游在处理数据的时候抛出异常,catch操作符就无法捕获到。我们再看一下下面这个示例:
fun main() {
runBlocking {
flow {
emit(1)
}.catch {
println("catch: $it")
}.map {
throw Exception("I am catch in map.")
it * 2
}
.collect {
println("collect: $it")
}
}
}
// 输出
Exception in thread "main" java.lang.Exception: I am catch in map.
...
运行这段代码我们会发现catch操作符无法捕捉到位于其下游map操作符中的异常。这也可能就是官网为啥把catch操作符废弃的原因吧。
10.异常操作符onCompletion
onCompletion操作符就是官网推出用来代替catch操作符的,它的好处在于它可以捕获到位于其下游的异常,其函数类型实例的Lambda中会接收一个可空类型的Throwable?参数,当没有异常时该参数为null,当有异常时该参数会正常传递。具体示例我们看如下代码:
fun main() {
runBlocking {
flow {
emit(1)
}.onCompletion { exception -> println("Flow completed with ${exception?.message}") }
.map {
it * it
}.collect { value ->
println("collect: $value")
}
}
}
// 输出
collect: 1
Flow completed with null
当有异常时:
fun main() {
runBlocking {
flow {
emit(1)
}.onCompletion { exception -> println("Flow completed with ${exception?.message}") }
.map {
throw Exception("I am catch in map.")
it * it
}.collect { value ->
println("collect: $value")
}
}
}
// 输出
Flow completed with I am catch in map.
Exception in thread "main" java.lang.Exception: I am catch in map.
...
虽然运行时仍会报错,但是下游的异常我们却可以正常捕获到,感兴趣的同学可以自己尝试下,如果在流上游出现异常onCompleiton操作符也是可以正常捕获到的。
总结
到这里本篇文章就把flow中一些常用的基础操作符介绍完了,下篇文章笔者打算介绍一下流中相对复杂的一些操作符。感谢您的观看~