前言
本篇文章主要介绍一下Flow常见操作符,同样的第一步,我们先引入Flow相关依赖:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
操作符
map
首先介绍一下map操作符,跟RxJava里面的map比较类似,我们看下代码:
val flow = flowOf(1, 2, 3, 4, 5)
//map
flow.map {
it+it
}.collect(){
println(it)
}
- 1
- 2
- 3
- 4
- 5
上面我们通过flowof,创建了一个数据流,然后map操作符变换,自身相加,然后打印数据,实际运行效果:
2 4 6 8 10
filter
filter 操作符,见名知意,就是过滤操作符
,看下代码:
val flow = flowOf(1, 2, 3, 4, 5)
flow.filter {
it % 2 == 0
}.map {
it*it
}.collect(){
println(it)
}
同样的,从建立的数据流,过滤了偶数,并通过map变换自身相乘,然后打印出来,看下运行效果:
4 16
onEach
onEach,简单来说就是遍历操作符,我们看下代码:
val flow = flowOf(1, 2, 3, 4, 5)
flow.onEach {
println(it)
}.collect(){
}
结果:
1 2 3 4 5
结合用
val flow = flowOf(1, 2, 3, 4, 5)
flow.filter {
it % 2 == 0
}.onEach {
println(it)
}.map {
it * it
}.collect {
println(it)
}
结果:
2
4
4
16
debounce
debounce翻译过来的意思,就是防抖
场景:
我们在搜索输入文字时,搜索框下面会显示对应的搜索结果,怎么去请求数据合适呢?
一直去请求吗,显然不合适,我们应该是在一定时间范围,用户不在输入时,去搜索比较合适吧,这样的场景下,debounce操作符比较合适。
例子:
flow {
emit(1)
emit(2)
delay(400)
emit(3)
delay(100)
emit(4)
delay(600)
emit(5)
}
.debounce(500)
.collect {
println(it)
}
分析:
我们看到上述代码,发射1 2 中间delay 400ms,再发射3 delay 100ms,再发射4 delay 600ms,再发射5,中间debounce 500ms,1、2连续发送和3间隔400ms,3和4间隔100ms,这个都没有超过500ms,所以他们不会发送成功,4和5间隔600ms超过500ms,4发送成功,5是最后一条数据,也会发送成功,所以正常会打印4、5,我们看下实际效果:
4
5
sample
sample操作符,简单来说就是采样.
每个一段时间,执行某个操作,比如我们每个一段时间采集一条弹幕,
代码:
flow {
while (true) {
emit("采集一条数据")
}
}
.sample(1000)
.flowOn(Dispatchers.IO)
.collect {
println(it)
}
上述代码,写了一个死循环,每隔一秒采集一条数据,数据流的采集操作放在IO线程,并打印出来,我们看下实际运行效果:
我们看到每隔一段时间,打印了采集一条数据,和我们设想的一样。
reduce
前面我们介绍的操作符,都是要借助collect来收集的,
readuce可以自己终结flow整个流程的操作符函数,也称终端操作符函数。
reduce操作符函数,包含两个参数
flow.reduce { acc, value -> acc + value } acc就是累积值,value当前值。
利用这个reduce函数我们可以做什么呢?
比如我们小时候经常遇到计算1-100相加的和 。
val result = flow {
for (i in (1..100)) {
emit(i)
}
}.reduce { acc, value ->
acc + value
}
println(result)
结果: 5050
fold
fold和reduce比较类似,只不过fold有个初始值,会后面的结果拼接起来。
val result = flow {
for (i in ('A'..'Z')) {
emit(i.toString())
}
}.fold("Ddup2024: ") { acc, value -> acc + value}
println(result)
上述代码的意思就是,A到Z拼接起来然后合并到"Ddup2024: "后面 。
flatMapConcat
多个Flow一起操作。
场景:
我们在开发中,常常会遇到嵌套请求的问题,比如我们去拿用户信息的时候,需要先请求Token数据,再拿Token去请求用户信息,这里使用flatMapConcat比较合适。
flowOf(1,2,3)
.flatMapConcat {
flowOf("A$it","B$it")
}.collect(){
println(it)
}
其实就是两个流按照flatMapConcat合并起来成一个flow并打印出来:
A1 B1 A2 B2 A3 B3
按照是第一个流顺序合并, 1 2 3 。 项目上可以这么写:
fun getTokenReq(): Flow<String> = flow {
// request to get token
emit(token)
}
fun getUserInfoReq(token: String): Flow<String> = flow {
// request with token to get user info
emit(userInfo)
}
getTokenReq()
.flatMapConcat { token ->
getUserInfoReq(token)
}
.flowOn(Dispatchers.IO)
.collect { userInfo ->
println(userInfo)
}
flatMapMerge
flatMapConcat和flatMapMerge区别:
flatMapMerge函数的内部是启用并发来进行数据处理的,它不会保证最终结果的顺序。
flatMapConcat 函数的内部是启用串行来进行数据处理的,它保证最终结果的顺序。
例子:
flowOf(300, 200, 100)
.flatMapMerge {
flow {
delay(it.toLong())
emit("a$it")
emit("b$it")
}
}
.collect {
println(it)
}
结果:
a200 b200
a300 b300
a100 b100
从打印的日志可以看出 没有按照第一个流的顺序合并。而是根据实际处理任务的时间先后顺序合并一起的。
flatMapLatest
flow1的数据发送过来,flow2会立即处理,但如果flow1的第二个数据发送过来,flow2第一个数据还没处理完,会直接丢弃第一个数据,去处理第二个数据。
例子:
flow {
emit(1)
delay(150)
emit(2)
delay(50)
emit(3)
}.flatMapLatest {
flow {
delay(100)
emit("$it")
}
}
.collect {
println(it)
}
这里,flow1 发送了1 2 3三个数据,flow2 100ms处理一条数据,我们可以看到1、2间隔了150ms,这时候1数据肯定处理完了,但是2数据还没处理完,3已经发送过来了,2就会丢弃,实际会打印1 3。
zip
zip函数也是两个流合并起来,和flatMap最大区别就是,zip是并行的,flatMap是一个流发送到另外一个流串行的。
tips:
这里要多讲讲, 那flatmap 不是有 flatmapMerge 和 flatmapMerge 区分么。他们是函数里面执行串行和并行。而zip 和flatmap 是流层面上的。
例子:
val flow1 = flowOf("a", "b", "c")
val flow2 = flowOf(1, 2, 3, 4, 5)
flow1.zip(flow2) { a, b ->
a + b
}.collect {
println(it)
}
我们可以看到flow1、flow2两个流数据量不一样,他们合并并不会像flatMap那样一一结合一起。
结果 : a1 b2 c3
buffer
我们之前使用RxJava,肯定听过背压问题,就是通过Observable发射,处理,响应数据流时,发送、处理数据之间速度不平衡,导致缓存池未来得及处理的数据就会造成积压,最终造成内存溢出,响应式编程中的背压(Backpressure)问题。同样的Flow,处理背压问题,会使用到介绍来介绍的三个函数。
flow {
emit(1);
delay(1000);
emit(2);
delay(1000);
emit(3);
}.onEach {
println("$it is ready")
}.collect {
delay(1000)
println("$it is handled")
}
我们在flow1发送每隔1秒发送了一条数据,onEach打印了发送的数据,然后我们花费一秒时间处理一条数据。
我们看下运行效果:
上述运行效果,也按我们前面说的一样执行,但是你有没有发现,我们2秒钟才处理了一条数据,我们稍加改造一下:
flow {
emit(1);
delay(1000);
emit(2);
delay(1000);
emit(3);
}.onEach {
println("$it is ready")
}.buffer()
.collect {
delay(1000)
println("$it is handled")
}
我们可以看到,上述代码,只是增加了一个buffer函数,我们再看下运行效果:
我们可以看到处理数据比之前相对较快了。
buffer函数的作用会让flow函数和collect函数运行在不同的协程当中,这样flow中的数据发送就不会受collect函数的影响了。
buffer函数其实提供了一块缓存区,当Flow数据流速不均匀的时候,使用这份缓存区来保证程序运行效率。
flow函数只管发送自己的数据,无需关心数据是否处理,反正数据会缓存在buffer中。
collect函数,只需要一直从buffer中获取数据,然后处理就行。
但是,可以思考这样一个问题,有必要每个数据都缓存下来,可不可以丢弃部分数据,丢弃数据如何处理呢?
接下来,我们可以了解一下conflate函数。
conflate
正和我们前面说的一样,处理数据流速不均的问题,我们可以采取丢弃部分数据来解决。
flow {
var count = 0
while (true) {
emit(count)
delay(1000)
count++
}
}.collectLatest {
println("start handle $it")
delay(2000)
println("finish handle $it")
}
这里我们使用了collectLatest函数,我们写了个死循环,每隔一秒改变一下count的值,collectLatest函数收集数据并打印,然后最后延迟2秒,打印完成处理。
我们看下实际运行效果:
我们可以看到一直在打印收集的最新的数据,但是处理完成的消息没有打印,collectLatest函数的特性就是当有新数据到来时而前一个数据还没有处理完,则会将前一个数据剩余的处理逻辑全部取消。
假如我们需要当前正在处理的数据无论如何都应该处理完,然后准备去处理下一条数据时,直接处理最新的数据即可,中间的数据就都可以丢弃掉了。我们可以使用到conflate函数。
前面的代码,改造如下:
flow {
var count = 0
while (true) {
emit(count)
delay(1000)
count++
}
}
.conflate()
.collect {
println("start handle $it")
delay(2000)
println("finish handle $it")
}
我们再运行一下,看下效果:
我们可以看到,每条数据都从头到尾处理完毕,中间错过的数据,直接丢弃,然后处理下条数据。
总结
至此,我们基本flow常见的函数操作符介绍完毕了。