玩转 Flow 操作符(一):数据转换与过滤

Flow 操作符指的是接收一个或多个上游 Flow,应用某种逻辑,然后返回一个全新的下游 Flow 的这类函数。

原始的 Flow 会保持不变。

过滤

过滤操作符会检查上游的每个元素,并决定是否要传到下游。

基础条件过滤

filter

filter 是最基础的过滤器,当它的 lambda 表达式返回 true 时,元素会被保留下来;反之返回 false,元素会被过滤。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf(1, 2, 3, 4, 5).filter { num ->
        num % 2 == 0 // 保留偶数
    }.collect {
        println("number is $it")
    }
}

运行结果:

less 复制代码
number is 2
number is 4

filterNot

filterNotfilter 的反向版本,判断逻辑反了而已:lambda 返回 true 时,元素会被过滤。

filterNotNull

filterNotNull 是一个非常实用的操作符,它可以过滤掉所有的 null 值,并且将 Flow 的类型从 Flow<T?> 转为 Flow<T>

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf(1, null, 3, 4, null).collect { num: Int? ->
        println("number is $num")
    }

    flowOf(1, null, 3, 4, null).filter {
        it != null
    }.collect { num: Int? ->
        println("filter number is $num")
    }

    flowOf(1, null, 3, 4, null).filterNotNull().collect { num: Int ->
        println("filterNotNull number is $num")
    }
}

运行结果:

less 复制代码
number is 1
number is null
number is 3
number is 4
number is null
filter number is 1
filter number is 3
filter number is 4
filterNotNull number is 1
filterNotNull number is 3
filterNotNull number is 4

filterIsInstance

filterIsInstance 可以根据元素的类型来过滤。

借助 Kotlin 的泛型实化,它能够将 Flow<Any> 转为 Flow<T>

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf(1, "hello", 2, "world")
        .filterIsInstance<String>()
        .collect {
            println(it)
        }
}

它还有一个版本,借助的是 kotlin.reflect.KClass<String> 实例:

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf(1, "hello", 2, "world")
        .filterIsInstance(String::class)
        .collect {
            println(it)
        }
}

有个陷阱需要注意:filterIsInstance 无法穿透 JVM 的类型擦除,所以不能过滤 List<String> 这样的嵌套泛型。

在运行时,它只能检查到元素类型为 List,无法区分 List<String>List<Int>,此时 .filterIsInstance<List<String>>().filterIsInstance(List::class) 的过滤效果相同。

如果要精确地过滤嵌套泛型,那么只能使用 filter 手动检查内部的元素

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf(1, listOf<String>("sunday", "monday"), listOf<Int>(6, 7, 8), "hello", 2, "world")
        .filter {
            it is List<*> &&
                    it.all { item -> item is String }
            // 如果是空列表,all 会返回 true
        }
        .collect {
            println(it)
        }
}

运行结果:

less 复制代码
[sunday, monday]

截断型过滤

这类操作符用于从数据流的开头或结尾截断数据。

  • take(count: Int) 是只获取前 count 个元素。

  • drop(count: Int) 是丢弃前 count 个元素。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf(1, 2, 3, 4, 5, 6)
        .take(4)
        .collect {
            println("tack(): $it")
        }

    flowOf(1, 2, 3, 4, 5, 6)
        .drop(3)
        .collect {
            println("drop(): $it")
        }
}

运行结果:

less 复制代码
tack(): 1
tack(): 2
tack(): 3
tack(): 4
drop(): 4
drop(): 5
drop(): 6
  • takeWhile {}:持续获取元素,直到返回 false。导致返回 false 的那个元素不会被保留。

  • dropWhile {}:持续丢弃元素,直到返回 false。导致返回 false 的那个元素以及后续元素都会被保留。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf(1, 2, 3, 4, 5, 6)
        .takeWhile { // 筛选所有小于等于3的元素
            it <= 3
        }
        .collect {
            println("takeWhile{}: $it")
        }

    flowOf(1, 2, 3, 4, 5, 6)
        .dropWhile { // 丢弃所有小于5的元素
            it < 5
        }
        .collect {
            println("dropWhile{}: $it")
        }
}

运行结果:

less 复制代码
takeWhile{}: 1
takeWhile{}: 2
takeWhile{}: 3
dropWhile{}: 5
dropWhile{}: 6

去重

distinctUntilChanged

distinctUntilChanged() 会过滤掉连续重复 的元素,它判断元素是否相等使用的是 equals(==) 方法。

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf(1, 1, 2, 2, 2, 3, 4, 4, 4)
        .distinctUntilChanged()
        .collect {
            println(it)
        }
}

运行结果:

less 复制代码
1
2
3
4

另外,我们可以定制判断是否相等的算法,只需调用该函数的另一个重载即可:

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf("example", "Example", "simple", "sample")
        .distinctUntilChanged { a, b ->
            a.lowercase() == b.lowercase()
        }
        .collect {
            println(it)
        }
}

运行结果:

less 复制代码
example
simple
sample

可以看到,即使 "example" 和 "Example" 并不一样,在我们的算法下,distinctUntilChanged() 函数还是认为两者重复。

distinctUntilChangedBy

distinctUntilChangedBy() 比较的并不是元素本身,而是比较由 keySelector lambda 生成的键。

还是刚刚的例子,我将 distinctUntilChanged() 换成了 distinctUntilChangedBy(),每个元素的键为转小写的元素值。

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf("example", "Example", "simple", "sample")
        .distinctUntilChangedBy { element ->
            element.lowercase()
        }
        .collect {
            println(it)
        }
}

运行结果还是和刚刚一样,因为 "Example" 的键 ("example") 与 "example" 的键 ("example") 相等,所以会被过滤掉。

转换

转换操作符可以将 Flow<T> 变为 Flow<R>

基础映射

map

map 是数学中"映射"的意思,它会对上游的每个元素应用 lambda 的逻辑,然后将映射后的元素传到下游。

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf("example", "Example", "simple", "sample")
        .map { element ->
            element.uppercase()
        }
        .collect {
            println(it)
        }
}

map 的执行(转换逻辑)是线性 的,它与上游的 emit() 和下游的 collect() 运行在同一个协程中。所以,上游在 emit 一个元素之后,当前协程会被挂起,直到 mapcollect 处理完该元素。

具体可以看我的这篇博客:Kotlin Flow 入门:构建响应式异步数据流,以了解 Flow 的核心原理。

这意味着同一时刻,整个 Flow 链中只有一个数据在被处理。

运行结果:

less 复制代码
EXAMPLE
EXAMPLE
SIMPLE
SAMPLE

mapNotNull

mapNotNullmap 的变体,它会过滤转换后为 null 的值。

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf("example", "Example", "simple", "sample")
        .mapNotNull { element ->
            val excludedString = listOf("example", "sample")
            val lowercaseElement = element.lowercase()
            if (excludedString.contains(lowercaseElement)) {
                null
            } else {
                lowercaseElement
            }
        }
        .collect {
            println(it)
        }
}

运行结果:

less 复制代码
simple

可以看出,这个操作符等价于 mapfilterNotNull 结合使用。

但它更适合当作过滤、映射操作符:需要过滤的数据就返回 null,需要保留的数据就进行转换后返回。

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf("example", "Example", "simple", "sample")
        .filter {
            val excludedString = listOf("example", "sample")
            val lowercaseElement = it.lowercase()
            !excludedString.contains(lowercaseElement)
        }.map {
            it.lowercase()
        }
        .collect {
            println(it)
        }
}

mapLatest

mapLatest 是一个并发操作符,它会启动新的子协程来执行转换逻辑,并且它会立马向上游请求下一个数据

它的效果是:当上游生产的新数据到来时,mapLatest 会取消上一个元素的转换逻辑(如果还在处理上一个元素),并开始处理最新的元素。

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf("example", "Example", "simple", "sample")
        .mapLatest {
            // 模拟耗时的转换逻辑
            delay(500)
            it.uppercase()
        }
        .collect {
            println(it)
        }
}

运行结果:

less 复制代码
SAMPLE

最终只会打印一个元素 "SAMPLE"。

它的应用场景很简单,就是只关系最新数据的场景。比如,搜索框提示:当用户快速输入多个字符时,之前触发的搜索请求就已经过时了,即使得到响应结果也没用。

高级转换:transform

transform

transform 就是更加底层的 map,但我们不再是 return 一个值,而是需要手动调用 emit() 将数据发送出去(lambda 提供了 FlowCollector 接收者)。

它提供了更多的自由度,比如我们可以模拟 mapfilter

kotlin 复制代码
fun main(): Unit = runBlocking {
    // 模拟 map,输出全部小写
    flowOf("example", "Example", "simple", "sample")
        .transform {
            emit(it.lowercase())
        }
        .collect {
            println(it)
        }

    // 模拟 filter,输出除了 excluded 中的元素
    flowOf("example", "Example", "simple", "sample")
        .transform { element ->
            val excluded = listOf("example", "sample")
            element.lowercase().let {
                if (!excluded.contains(it)) {
                    emit(it)
                }
            }
        }.collect {
            println(it)
        }
}

并且,可以控制单个上游元素所触发的下游发送次数(一对多):

kotlin 复制代码
fun main(): Unit = runBlocking {
    flowOf(1, 2)
        .transform { value ->
            emit("Item: $value")
            if (value == 2) {
                emit("Item 2 also emits this")
            }
        }.collect {
            println(it)
        }
}

transformWhile

transformWhile {...} 相当于是 transformtakeWhile 的结合,当 lambda 返回 false 后会停止处理。

这个操作符适合需要持续对上游数据进行一对多转换,直到发现某个条件满足,然后停止整个数据流的场景。

kotlin 复制代码
fun main() = runBlocking {
    val numbers = flowOf(1, 2, 3, 4, 5)

    numbers
        .transformWhile { number ->
            // 转换
            emit("Processing: $number")

            // 如果当前数字为 3,就返回 false 来停止
            if (number == 3) {
                println("(Processed 3, stopped)")
                false // 返回 false,Flow 立即终止
            } else {
                true  // 返回 true,继续处理下一个
            }
        }.collect {
            println(it)
        }
}

运行结果:

less 复制代码
Processing: 1
Processing: 2
Processing: 3
(Processed 3, stopped)

transformLatest

transformLatestmapLatest 的底层版本,同样会取消旧的转换逻辑。

辅助与调试

withIndex

withIndex() 用于给上游的每个元素编号,它会将 Flow<T> 转为 Flow<IndexedValue<T>>

kotlin 复制代码
fun main() = runBlocking {
    val numbers = flowOf(1, 2, 3, 4, 5)
    numbers.withIndex()
        .collect { indexedValue ->
            println("index: ${indexedValue.index}, value: ${indexedValue.value}")
        }
}

运行结果:

less 复制代码
index: 0, value: 1
index: 1, value: 2
index: 2, value: 3
index: 3, value: 4
index: 4, value: 5

collectIndexed 也可新增索引,不过它是末端操作符,而 withIndex 是中间操作符。

onEach

onEach 是"窥探"(Peek)操作符,它不会修改数据流,而是每当元素到达时,会执行一个动作。

onEach 常常和 launchIn 配合使用。

注意,需要有 launchIncollect 等末端操作符的调用,onEach 中的代码才会执行。

kotlin 复制代码
fun main(): Unit = runBlocking {
    val numbers = flowOf(1, 2, 3, 4, 5)
    numbers.onEach {
        println("First onEach: $it")
    }.filter {
        it % 2 == 0
    }.onEach {
        println("Second onEach: $it")
    }.launchIn(this)
}

运行结果:

less 复制代码
First onEach: 1
First onEach: 2
Second onEach: 2
First onEach: 3
First onEach: 4
Second onEach: 4
First onEach: 5

自定义操作符

一个操作符就是一个 Flow 的扩展函数,它会返一个新的 Flow

kotlin 复制代码
// 自定义一个操作符,它会重复发送每个元素两次
fun <T> Flow<T>.emitTwice(): Flow<T> = flow { // 或者使用 channelFlow 创建
    // 在当前 Flow 中收集原始 Flow 的元素
    this@emitTwice.collect {
        // 发送每个元素两次
        this.emit(it)
        this.emit(it)
    }
}
相关推荐
二流小码农3 小时前
鸿蒙开发:web页面如何适配深色模式
android·ios·harmonyos
消失的旧时光-19435 小时前
TCP 流通信中的 EOFException 与 JSON 半包问题解析
android·json·tcp·数据
JiaoJunfeng6 小时前
android 8以上桌面图标适配方案(圆形)
android·图标适配
参宿四南河三6 小时前
Android Compose快速入门手册(真的只是入门)
android·app
芦半山7 小时前
Looper究竟在等什么?
android
czhc11400756639 小时前
JAVA1027抽象类;抽象类继承
android·java·开发语言
_Sem9 小时前
KMP实战:从单端到跨平台的完整迁移指南
android·前端·app
從南走到北9 小时前
JAVA国际版任务悬赏发布接单系统源码支持IOS+Android+H5
android·java·ios·微信·微信小程序·小程序
vistaup10 小时前
Android ContentProvier
android·数据库