Day18:Flow 的中间运算子,资料输出前还可以做很多事喔

我们在上一篇的文章中,介绍了 Flow 的基本概念,包括如何建立一个 Flow,以及 Flow 是一个 Cold stream,所谓的 cold stream 就是在当我们呼叫 collect{} 的时候,这串资料才会被执行。

Flow 厉害的一个点就在於,我们可以在呼叫 collect{} 之前利用一些操作,让资料转换成别的样子,这个操作称之为 Intermediate operators (中间运算子),这边的操作与 Functional Programming 一样,都是可以在输出之前做一些操作,让原本的资料流转换成新的样子。而 Flow 在设计这些函式的时候,也按照 Functional Programming 定义的名称,所以熟悉 FP 的朋友应该对於 Flow 提供的中间操作不会太陌生。

Intermediate operators (中间运算子)

这是我们前一篇的范例,

fun flow(): Flow<Int> = flow {
      println("Flow started")
      repeat(10){
        delay(100)
        emit(it)
    }
}

fun main() = runBlocking {
    val flow = flow()
    flow.collect { value -> println(value)}
}

根据我们昨天所介绍的,当我们呼叫 collect{ value -> println(value) } 的时候,我们将会列印出 0~10。(因为我们在 flow{} 里面,按照顺序由 0 ~ 9 每间隔 100 毫秒发送当下的整数值进入 flow 中。)

map

map 其实是 mapping 的意思,也就是说我们可以将输入的值按照我们设定的方式对应到某一个 Domain 中。

如下图,我们利用 map 来把 X映射到 X² 的 Domain。

Flow map

inline fun <T, R> Flow<T>.map(crossinline transform: suspend (T) -> R): Flow<R>

map 的定义我们也可以发现,输入的型别为 T 经过转换之後,会变成 R,也就是转成另外一个 Domain 上。

使用范例:

fun main() = runBlocking {
    val flow = flow()
    flow.map{ it*it }
        .collect { println(it)}
}
Flow started
0
1
4
9
16
25
36
49
64
81

filter

filter 如同它的名称,就是用来过滤的,在这边我们可以自定义过滤的条件。我们先看一下它的签名:

inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T>

在 filter 中,我们带入的是一个回传 Boolean 的 lambda,当传入的值满足条件时,就会把这个值往下传,否则就会被过滤下来。

范例如下:

我们尝试把偶数过滤出来,我们可以这麽做

fun main() = runBlocking {
    val flow = flow()
    flow.filter{ it % 2 == 0 }
        .collect { println(it)}
}
Flow started
0
2
4
6
8

take

将资料流只保留相对应的数量,当指定的数量超过资料流的数量时,就会以资料流的数量为主,但是如果带入的是负值的话,就会抛出 java.lang.IllegalArgumentException

同样的,我们看一下 take 的签名:

fun <T> Flow<T>.take(count: Int): Flow<T>

在 take 中,参数 count: Int 就是用来指定要取得的数量。

范例:

取得资料流中,前三个数值

fun main() = runBlocking {
    val flow = flow()
    flow.take(3)
        .collect { println(it)}
}
Flow started
0
1
2

那麽,不知道你们有没有注意到, take 是由资料流的最前面开始取,所以取得前三个值就会是 0 1 2

zip

zip 是用来把两个 Flow 组合起来,我们看一下它的签名:

fun <T1, T2, R> Flow<T1>.zip(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R>

不罗唆,我们直接看范例:

fun main() = runBlocking{
    val stringFlow = listOf("a", "b", "c", "d").asFlow()
    val intFlow = (1..3).asFlow()
    intFlow.zip(stringFlow){int, string -> "$int-$string"}
            .collect { println(it) }
}
1-a
2-b
3-c

在这个范例中,我们有两个 flow,一个是含有四个元素("a", "b", "c", "d")的 stringFlow,另一个则是含有三个元素(1, 2, 3) 的 flow。我们想要将两组资料流组合在一起,我们使用 intFlow 的 zip 来组合。

从上方的范例我们可以清楚的得知,zip 组合而成的数量会与这两个 flow 最少的相同,因为 zip 需要的是一对一的组合,所以没有办法组合的部分就会被舍弃。

combine

zip 类似, combine 也是用来组合 Flow 的函式,将上方的范例改成使用 combine 看看结果如何。

fun main() = runBlocking{
    val stringFlow = listOf("a", "b", "c", "d").asFlow()
    val intFlow = (1..3).asFlow()
    intFlow.combine(stringFlow){int, string -> "$int-$string"}
        .collect { println(it) }
}
1-a
2-b
3-c
3-d

执行之後发现,虽然与 zip 一样都是结合 flow 的函式,但是两者的结果不同, combine 所产生的数量会与这两个 flow 中最多的一样,当组合元素不够的时候,就会拿前一个值来使用。

另外,与 zip 不同的是 combine 不只可以组合两个 flow,它支援组合多个 flow。签名如下:

fun <T1, T2, R> Flow<T1>.combine(flow: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R>
fun <T1, T2, R> combine(flow: Flow<T1>, flow2: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R>
fun <T1, T2, T3, R> combine(flow: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, transform: suspend (T1, T2, T3) -> R): Flow<R>
fun <T1, T2, T3, T4, R> combine(flow: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, flow4: Flow<T4>, transform: suspend (T1, T2, T3, T4) -> R): Flow<R>
fun <T1, T2, T3, T4, T5, R> combine(flow: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, flow4: Flow<T4>, flow5: Flow<T5>, transform: suspend (T1, T2, T3, T4, T5) -> R): Flow<R>
inline fun <T, R> combine(vararg flows: Flow<T>, crossinline transform: suspend (Array<T>) -> R): Flow<R>
inline fun <T, R> combine(flows: Iterable<Flow<T>>, crossinline transform: suspend (Array<T>) -> R): Flow<R>

我们在前面介绍了几个中间运算子,这些运算子是可以组合使用的,如下:

fun main() = runBlocking{
    val flow = flow()
    flow.map { it * 3 }
    .filter { it % 2 == 0 }
    .take(2)
    .collect { println(it) }
}
Flow started
0
6

从结果只列印一个 Flow started 得知,原本的 Flow 只会被执行一次,每经过一个中间运算子都会产生一组新的 Flow,这样子一层一层的把 Flow 往下送,直到最後的 collect{}

小结

Flow 可以让你使用串串大法,把资料在输出前经过一层一层的处理产生出我们想要的样子,这样子的好处是我们可以用更简洁的方式来完成我们所需要的操作。
另外, Flow 所提供的这些中间运算子的名称与 Functional Programming 里面的相同,原因是因为它们属於相同的操作,如果熟悉 FP 对这样的操作就不会太陌生。

本篇文章先介绍到这边,下篇文章将继续介绍 Flow ,除了 collect 以外,我们还有哪些运算子可以使用呢?

特别感谢

Kotlin Taiwan User Group
Kotlin 读书会


<<:  Day 08 - Design System x 实作 — Color System

>>:  有关版本控制

Day.2 选择 - 关联式与非关联式 (SQL vs. NoSQL )

提到资料库特性势必要先了解SQL(关联式资料库)vs.NoSQL(非关联式资料库)之间的差异,在应...

React的秘密-原理解析第壹篇:核心概念

作为壹个构建用户界面的库,React的核心始终围绕着更新这壹个重要的目标,将更新和极致的用户体验结合...

[Day 29] - 手把手跨出第一步!– 烧录闪烁程序到Arduino Part.2

17King 制造中,订阅一下吧(*´∀`)~♥ 今日影片长度:08 分 36 秒 上集重点条列: ...

【day13】连续上班日做便当2

今天的便当是无淀粉系列 主菜是鲜甜的肉束尾 其实我本人很害怕猪肉的腥味 但男友妈妈准备的食材都很好 ...

[Day11] TS:什麽!型别也有样板字串(Template Literal Types)?

这是我们今天要聊的内容,老样的,如果你已经可以轻松看懂,欢迎直接左转去看我同事 Kyle 精彩的文...