kotlin Flow 学习指南(二)

前言

上篇文章,介绍了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操作符变换,自身相加,然后打印数据,实际运行效果:

image.png

filter

filter 操作符,见名知意,就是过滤操作符,看下代码:

val flow = flowOf(1, 2, 3, 4, 5)
flow.filter {
    it % 2 == 0
}.map {
    it*it
}.collect(){
    println(it)
}

同样的,从建立的数据流,过滤了偶数,并通过map变换自身相乘,然后打印出来,看下运行效果:

image.png
没有问题,与我们期望的效果一样。

onEach

onEach,简单来说就是遍历操作符,我们看下代码:

flow.onEach {
    println(it)
}.collect(){

}

上述代码,就是遍历了一遍数据流,并打印出来,看下实际运行效果:

image.png
没有问题,结合前面两个操作符,一起试下:

flow.filter {
    it % 2 == 0
}.onEach {
    println(it)
}.map {
    it * it
}.collect {
}

上述代码,在变换前,打印过程中的数据流元素,看下实际运行效果:

image.png
可以看到,过滤了偶数,并且在map变换前,打印了数据,简单来说onEach操作符适合在一些操作的中间过程中,遍历对应的原始数据。

debounce

debounce翻译过来的意思,就是防抖,我们设想一个场景,我们在搜索输入文字时,搜索框下面会显示对应的搜索结果,怎么去请求数据合适呢?

image.png
一直去请求吗,显然不合适,我们应该是在一定时间范围,用户不在输入时,去搜索比较合适吧,这样的场景下,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,我们看下实际效果:

image.png
果不其然,和我们分析的结果一样。

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)

运行效果:

image.png

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: "后面,看下实际效果:

image.png
ok,没有问题。

flatMapConcat

我们前面介绍的操作符,都是单独一个Flow,我们现在要介绍的是,多个Flow一起操作。我们再开发中,常常会遇到嵌套请求的问题,比如我们去拿用户信息的时候,需要先请求Token数据,再拿Token去请求用户信息,这里使用flatMapConcat比较合适,我们先看一个简单例子:

flowOf(1,2,3)
    .flatMapConcat {
        flowOf("A$it","B$it")
    }.collect(){
        println(it)
    }

上述代码,其实就是两个流按照flatMapConcat合并起来成一个flow并打印出来,看下实际运行效果:

image.png
我们看到两个流合并的时候,是按照第一个流的顺序合并起来的,由此可见,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)
    }

看下实际运行效果:

image.png
我们从打印的日志可以看到,他没有按照第一个流的顺序合并。而是根据实际处理任务的时间先后顺序合并一起的。

flatMapLatest

我们掌握flatMapConcatflatMapMerge两个函数,接下来了解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。

image.png
到这里,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那样一一结合一起。

image.png
上述只打印了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将两个流合并起来,最终代码执行时间多久呢,我们运行看下效果:

image.png
我们可以看到执行时间耗费了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打印了发送的数据,然后我们花费一秒时间处理一条数据。
我们看下运行效果:

buffer.gif

上述运行效果,也按我们前面说的一样执行,但是你有没有发现,我们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秒,打印完成处理。
我们看下实际运行效果:

conflate1.gif

我们可以看到一直在打印收集的最新的数据,但是处理完成的消息没有打印,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")
    }

我们再运行一下,看下效果:

conflate2.gif

我们可以看到,每条数据都从头到尾处理完毕,中间错过的数据,直接丢弃,然后处理下条数据。

总结

至此,我们基本flow常见的函数操作符介绍完毕了,我们可以结合一些业务场景按需使用,这里主要是通过一些例子,简单介绍了一下flow函数的操作符,我们实际项目使用最多的还是StateFlow、SharedFlow这些。
下面文章,我将从实际项目例子介绍一下StateFlow、SharedFlow相关用法。
创作不易,喜欢的麻烦点赞、评论,以资鼓励。

参考文章
Kotlin Flow响应式编程,操作符函数进阶

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值