目录
前言
上篇文章,介绍了Flow基本用法,本篇文章主要介绍一下Flow常见操作符,同样的第一步,我们先引入Flow相关依赖:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
由于Flow,只和kotlin相关,本文新建一个Flow kotlin类验证操作符效果:
fun main() {
runBlocking {
}
}
map
首先介绍一下map操作符,跟RxJava里面的map比较类似,我们看下代码:
val flow = flowOf(1, 2, 3, 4, 5)
//map
flow.map {
it+it
}.collect(){
println(it)
}
上面我们通过flowof,创建了一个数据流,然后map操作符变换,自身相加,然后打印数据,实际运行效果:
filter
filter 操作符,见名知意,就是过滤操作符,看下代码:
val flow = flowOf(1, 2, 3, 4, 5)
flow.filter {
it % 2 == 0
}.map {
it*it
}.collect(){
println(it)
}
同样的,从建立的数据流,过滤了偶数,并通过map变换自身相乘,然后打印出来,看下运行效果:
没有问题,与我们期望的效果一样。
onEach
onEach,简单来说就是遍历操作符,我们看下代码:
flow.onEach {
println(it)
}.collect(){
}
上述代码,就是遍历了一遍数据流,并打印出来,看下实际运行效果:
没有问题,结合前面两个操作符,一起试下:
flow.filter {
it % 2 == 0
}.onEach {
println(it)
}.map {
it * it
}.collect {
}
上述代码,在变换前,打印过程中的数据流元素,看下实际运行效果:
可以看到,过滤了偶数,并且在map变换前,打印了数据,简单来说onEach操作符适合在一些操作的中间过程中,遍历对应的原始数据。
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,我们看下实际效果:
果不其然,和我们分析的结果一样。
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相加的和,这个使用reduce实现如下:
val result = flow {
for (i in (1..100)) {
emit(i)
}
}.reduce { acc, value ->
acc + value
}
println(result)
运行效果:
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: "后面,看下实际效果:
ok,没有问题。
flatMapConcat
我们前面介绍的操作符,都是单独一个Flow,我们现在要介绍的是,多个Flow一起操作。我们再开发中,常常会遇到嵌套请求的问题,比如我们去拿用户信息的时候,需要先请求Token数据,再拿Token去请求用户信息,这里使用flatMapConcat比较合适,我们先看一个简单例子:
flowOf(1,2,3)
.flatMapConcat {
flowOf("A$it","B$it")
}.collect(){
println(it)
}
上述代码,其实就是两个流按照flatMapConcat合并起来成一个flow并打印出来,看下实际运行效果:
我们看到两个流合并的时候,是按照第一个流的顺序合并起来的,由此可见,flatMapConcat
连接一定会保证数据是按照原有的顺序连接起来的。
结合上面的例子,我们用伪代码实现一下,之前说的嵌套请求:
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函数的内部是启用并发来进行数据处理的,它不会保证最终结果的顺序。同样的看个简单的例子:
flowOf(300, 200, 100)
.flatMapMerge {
flow {
delay(it.toLong())
emit("a$it")
emit("b$it")
}
}
.collect {
println(it)
}
看下实际运行效果:
我们从打印的日志可以看到,他没有按照第一个流的顺序合并。而是根据实际处理任务的时间先后顺序合并一起的。
flatMapLatest
我们掌握flatMapConcat、flatMapMerge两个函数,接下来了解flatMapLatest就比较容易了,它和上篇介绍的collectLatest函数比较类似,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。
到这里,flatMap函数都介绍完了。
zip
zip函数也是两个流合并起来,和flatMap最大区别就是,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那样一一结合一起。
上述只打印了3条数据,并不像flatMap一样,一一串行连接起来,我们再看个例子更明显一点:
val start = System.currentTimeMillis()
val flow1 = flow {
delay(3000)
emit("a")
}
val flow2 = flow {
delay(2000)
emit(1)
}
flow1.zip(flow2) { a, b ->
a + b
}.collect {
val end = System.currentTimeMillis()
println(it)
println("Time cost: ${end - start}ms")
}
我们创建了两个流,一个流花费了3000ms,一个流花费了2000ms,通过zip将两个流合并起来,最终代码执行时间多久呢,我们运行看下效果:
我们可以看到执行时间耗费了3000ms左右,证明zip函数是并发合并的,如果是串行的将耗费5000ms左右。
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
正和我们前面说的一样,处理数据流速不均的问题,我们可以采取丢弃部分数据来解决。
我们先看下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常见的函数操作符介绍完毕了,我们可以结合一些业务场景按需使用,这里主要是通过一些例子,简单介绍了一下flow函数的操作符,我们实际项目使用最多的还是StateFlow、SharedFlow这些。
下面文章,我将从实际项目例子介绍一下StateFlow、SharedFlow相关用法。
创作不易,喜欢的麻烦点赞、评论,以资鼓励。