Kotlin 中的 Flow 是一个用于处理异步数据流的抽象,它是 Kotlin 协程库的一部分。Flow 提供了一种声明性的方式来处理序列化的异步数据,如从数据库、网络请求或任何其他异步数据源中获取的数据。Flow 使得处理这些数据流变得更加简洁和高效。
1,基本概念:
Flow:一个冷数据流,它表示一个异步的、序列化的值 T 的流。
发射(Emit):Flow 通过发射(emit)函数来产生数据项。
收集(Collect):Flow 的数据项通过 collect 函数来消费。
Flow 是一个冷数据流,这意味着它只有在被收集(collect)时才会开始执行。这与热数据流(如 LiveData 或 RxJava 的 Observable)不同,后者即使没有观察者也会继续发射数据。
2,创建Flow与收集Flow:
使用 flow{} 构建器创建,并可以使用asFlow()扩展函数将现有的集合或序列转换为 Flow。
使用 collect 函数来消费 Flow 中的数据。collect 是一个挂起函数,需要在协程作用域内调用。
val flowA = flow {
emit(1)
emit(2)
emit(3)
emit("hello")
emit("world")
}
val list = listOf(1,2,3,4,"Jacky","Jerry")
val flowB = list.asFlow()
runBlocking {
flowA.collect { valueA ->
println(valueA)
}
flowB.collect { valueB ->
println(valueB)
}
}
Flow 的生命周期与它的收集者紧密相关。一旦收集者被取消或完成,Flow 的执行也会相应停止。
使用 withTimeout 在超时后取消 Flow 的代码示例:
val list = listOf(1,2,3,4,"Jacky","Mary")
val flowB = list.asFlow()
runBlocking {
try {
withTimeout(3000L) {
flowB.collect { valueB ->
delay(2000L)
println(valueB)
}
}
} catch (e:TimeoutCancellationException) {
println("TimeoutCancellationException...")
}
}
3,Flow的操作符
Flow 提供了丰富的操作符,用于转换和组合数据流。
3.1,转换操作符,包括 map,filter,take,drop等。
map:将 Flow 中的每个值转换为另一个值。
import kotlinx.coroutines.flow.*
runBlocking {
val numbers = flowOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it }
squaredNumbers.collect { println(it) } // 输出: 1, 4, 9, 16, 25
}
filter:只允许满足特定条件的值通过 Flow。
runBlocking {
val numbers = flowOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.filter { it % 2 == 0 }
squaredNumbers.collect { println(it) } // 输出: 2, 4
}
take:从 Flow 中获取指定数量的值。
runBlocking {
val numbers = flowOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.take(3)
evenNumbers.collect { println(it) } // 输出: 1, 2, 3
}
drop:跳过 Flow 中的指定数量的值。
runBlocking {
val numbers = flowOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.drop(3)
evenNumbers.collect { println(it) } // 输出: 4, 5
}
3.2,组合操作符,包括 zip、merge、combine、flattenMerge 等。
zip:将两个 Flow 的值按对组合起来。
runBlocking {
val numbers = flowOf(1, 2, 3)
val letters = flowOf('a', 'b', 'c')
val zipped = numbers.zip(letters) { num, letter -> "$num$letter" }
zipped.collect { println(it) } // 输出: 1a, 2b, 3c
val zippedB = numbers.zip(letters) { num, letter -> "$letter$num$letter" }
zippedB.collect { println(it) } // 输出: a1a, b2b, c3c
}
merge:将两个或多个 Flow 的值合并到一个 Flow 中,值的顺序不确定。
runBlocking {
val flow1 = flowOf(1, 3, 5).onEach { delay(100) } // 模拟延迟
val flow2 = flowOf(2, 4, 6).onEach { delay(150) }
val merged = merge(flow1, flow2)
merged.collect { println(it) } // 输出顺序可能是: 1, 2, 3, 4, 5, 6 或其他
}
combine:将多个 Flow 的最新值组合起来,并在任何一个 Flow 发射新值时更新结果。
runBlocking {
val flowA = flowOf(1, 2).map { "A$it" }
val flowB = flowOf(10, 20).map { "B$it" }
val combined = combine(flowA, flowB) { a, b -> "$a and $b" }
combined.collect { println(it) } // 输出: A1 and B10, A2 and B20
}
flattenMerge:将一个发射 Flow 的 Flow 转换为一个包含所有内部 Flow 值的单个 Flow。
runBlocking {
val flowOfFlows = flowOf(
flowOf(1, 2),
flowOf(3, 4, 5)
)
val flattened = flowOfFlows.flattenMerge()
flattened.collect { println(it) } // 输出: 1, 2, 3, 4, 5(顺序不确定)
}
上面的代码中,
runBlocking是一个挂起函数,它启动一个新的协程作用域并阻塞当前线程,直到该作用域内的所有协程都完成。这是为了在阻塞线程环境中(如main函数)运行挂起函数(如Flow的collect操作)的常用方式。
flowOf(flowOf(1, 2), flowOf(3, 4, 5))创建了一个Flow的Flow,即一个包含两个内部Flow的外部Flow。第一个内部Flow发出1和2,第二个内部Flow发出3、4和5。
flattenMerge()是一个操作符,用于将嵌套的Flow展平成一个单一的Flow,并合并来自所有内部Flow的元素。
与flattenConcat()不同,flattenMerge()不会保持来自内部Flow的元素顺序,而是并发地收集它们,因此最终输出的顺序是不确定的。
3.3,flattenConcat:
flattenConcat 是 Kotlin 协程中 Flow API 的一部分,用于将嵌套的 Flow(即 Flow 的 Flow)展平成一个单一的 Flow,并顺序地连接来自内部 Flow 的元素。
与 flattenMerge 不同,flattenConcat 会保持内部 Flow 发出元素的顺序,即它会先收集完第一个内部 Flow 的所有元素,然后再开始收集第二个内部 Flow 的元素,以此类推。
runBlocking {
// 创建一个嵌套的 Flow,包含两个内部 Flow
val flowOfFlows: Flow<Flow<Int>> = flowOf(
flowOf(1, 2).flowOn(Dispatchers.IO), // 在 IO 调度器上运行
flowOf(3, 4, 5).flowOn(Dispatchers.Default) // 在 Default 调度器上运行
)
// 使用 flattenConcat 展平嵌套的 Flow
val flattened: Flow<Int> = flowOfFlows.flattenConcat()
// 收集并打印展平后的 Flow 中的元素
flattened.collect { value ->
println("Collected: $value")
}
}
上面的代码中, 创建了一个 flowOfFlows,它是一个包含两个内部 Flow 的外部 Flow。每个内部 Flow 都在不同的协程调度器上运行(Dispatchers.IO 和 Dispatchers.Default), 但这并不影响 flattenConcat 的行为,因为 flattenConcat 关心的是元素的顺序,而不是它们在哪个线程或调度器上被发出。