认识 Flow
如何表示多个值?
挂起函数可以异步的返回单个值,但是该如何异步返回多个计算好的值呢?
首先来看看集合如何返回多个值:
fun main() {
simpleList().forEach { value ->
println(value)
}
}
fun simpleList() : List<Int> = listOf(1, 2, 3)
//1
//2
//3
集合虽然返回了多个值,但是不是异步的。
我们来看下如何用序列返回多个值:
fun main() {
simpleSequence().forEach { value ->
println(value)
}
}
fun simpleSequence() : Sequence<Int> = sequence {
for (i in 1..3) {
Thread.sleep(1000)
yield(i)
}
}
//1
//2
//3
在这个例子里我们延时1000ms来假装在计算,之后输出值,但是这里是线程阻塞的,并不是异步的。而且这里并不能使用 delay() 函数,受 RestrictsSuspension 注解的约束,delay 不能在 SequenceScope 的扩展成员当中被调用,因而不能在序列生成器的协程体内调用了。
如果我们使用集合➕挂起函数呢?
suspend fun main() {
simpleList2().forEach { value ->
println(value)
}
}
suspend fun simpleList2() : List<Int> {
delay(1000)
return listOf(1, 2, 3)
}
//1
//2
//3
这里虽然返回了多个值,也是异步,但是是一次性返回了多个值。那如何一次只给一个值呢? 我们来看看使用 Flow 的方式:
suspend fun main() {
simpleFlow().collect { value ->
println(value)
}
}
fun simpleFlow() = flow<Int> {
for (i in 1..3) {
delay(1000)
emit(i) // 发射,产生一个元素
}
}
//1
//2
//3
这里我们每隔1s拿到一个元素,和 sequence 很像,但是这里是异步的,sequence 是阻塞的。
这里总结一下:
- 名为 flow 的 Flow 类型构建器函数。
- flow{...} 构建块中的代码可以挂起。
- 函数 simpleFlow 不再标有 suspend 修饰符。
- 流使用 emit 函数发射值。
- 流使用 collect 函数收集值。
冷数据流
Flow 是一种类似于序列的冷数据流,flow 构建器中的代码直到流被收集的时候菜运行,不消费则不生产,多次消费则多次生产,生产和消费总是相对应的。
suspend fun main() {
val flow = simpleFlow2()
println("Calling collect...")
flow.collect { value -> println(value) }
println("Calling collect again...")
flow.collect { value -> println(value) }
}
fun simpleFlow2() = flow<Int> {
println("Flow started")
for (i in 1..3) {
kotlinx.coroutines.delay(1000)
emit(i)
}
}
//Calling collect...
//Flow started
//1
//2
//3
//Calling collect again...
//Flow started
//1
//2
//3
Flow 的连续性
流点的每次单独收集都是按顺序执行的,除非使用特殊操作符。
从上游到下游每个过渡操作符都会处理每隔发射出的值,然后再交给末端操作符。
suspend fun main() {
(1..5).asFlow().filter {
it % 2 == 0
}.map {
"string $it"
}.collect {
println("Collect $it")
}
}
//Collect string 2
//Collect string 4
Flow 的构建器
flowOf 构建器定义了一个发射固定值集的流。
下面每隔1s输出一个值:
suspend fun main() {
flowOf("one", "two", "three")
.onEach { delay(1000) }
.collect {
println(it)
}
}
//one
//two
//three
使用 .asFlow() 扩展函数,可以将各种集合与序列转换为流。
suspend fun main() {
(1..3).asFlow().collect {
println(it)
}
}
//1
//2
//3
Flow 的上下文
流的收集总是在调用协程的上下文中发生,流的该属性称为上下文保存;
flow{...} 构建器中的代码必须遵循上下文保存属性,并且不允许从其他上下文中发射(emit);
flowOn操作符,该函数用于更改流发射的上下文。
suspend fun main() {
simpleFlow3().collect {
println("Collected $it ${Thread.currentThread().name}")
}
}
fun simpleFlow3() = flow<Int> {
println("Flow started ${Thread.currentThread().name}")
for (i in 1..3) {
kotlinx.coroutines.delay(1000)
emit(i)
}
}.flowOn(Dispatchers.Default)
//Flow started DefaultDispatcher-worker-1
//Collected 1 DefaultDispatcher-worker-1
//Collected 2 DefaultDispatcher-worker-1
//Collected 3 DefaultDispatcher-worker-1
在指定协程收集流
使用 launchIn 替换 collect 我们可以在单独的协程中启动流的收集。
suspend fun main() {
events().onEach { event -> println("Event: $event ${Thread.currentThread().name}") }
.launchIn(CoroutineScope(Dispatchers.IO))
.join()
}
fun events() = (1..3)
.asFlow()
.onEach { delay(1000) }
.flowOn(Dispatchers.Default)
//Event: 1 DefaultDispatcher-worker-1
//Event: 2 DefaultDispatcher-worker-2
//Event: 3 DefaultDispatcher-worker-1
Flow 的取消
流采用与协程同样的协作取消,流的取消可以是当流在一个可取消的挂起函数(例如 delay)中挂起的时候取消,Flow 的消费依赖于 collect 这样的末端操作符,而它们又必须在协程当中调用,因此 Flow 的取消主要依赖于末端操作符所在的协程的状态。
suspend fun main() {
withTimeoutOrNull(2500) {
simpleFlow4().collect { println(it) }
}
println("Done")
}
fun simpleFlow4() = flow<Int> {
for (i in 1..3) {
delay(1000)
emit(i)
println("Emitting $i")
}
}
//1
//Emitting 1
//2
//Emitting 2
//Done
Flow 的取消检测
为方便起见,流构建器对每个发射值执行附加的 ensureActive 检测以进行取消,这意味着从 flow {...} 发出的繁忙循环是可以取消的。
fun main() = runBlocking {
simpleFlow5().collect {
println(it)
if (it == 3) cancel()
}
}
fun simpleFlow5() = flow<Int> {
for (i in 1..5) {
emit(i)
println("Emitting $i")
}
}
//1
//Emitting 1
//2
//Emitting 2
//3
//Emitting 3
//Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@3d121db3
出于性能原因,大多数其他流操作不会自行执行其他取消检测,在协程处于繁忙循环的情况下,必须检测是否取消。
fun main() = runBlocking {
(1..5).asFlow().collect {
println(it)
if (it == 3) cancel()
}
}
//1
//2
//3
//4
//5
//Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@6e75aa0d
通过 cancellable 操作符来执行此操作。
fun main() = runBlocking {
(1..5).asFlow().cancellable().collect {
println(it)
if (it == 3) cancel()
}
}
//1
//2
//3
//Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@6e75aa0d
Flow 的背压
背压问题在生产者的生产速率高于消费者的处理速率的情况下出现。那我们如何处理背压问题呢?
buffer(),并发运行流中发射元素的代码。
fun main() = runBlocking {
val time = measureTimeMillis {
simpleFlow6().collect {
delay(300)
println("Collected $it ${Thread.currentThread().name}")
}
}
println("Collected in $time ms")
}
fun simpleFlow6() = flow<Int> {
for (i in 1..3) {
delay(100)
emit(i)
println("Emitting $i ${Thread.currentThread().name}")
}
}
//Collected 1 main
//Emitting 1 main
//Collected 2 main
//Emitting 2 main
//Collected 3 main
//Emitting 3 main
//Collected in 1268 ms
发送需要100ms,接收需要300ms,处理三个数据一共花费1268ms,我们可以使用buffer()进行优化:
simpleFlow6()
.buffer(50)
.collect {
delay(300)
println("Collected $it ${Thread.currentThread().name}")
}
//Emitting 1 main
//Emitting 2 main
//Emitting 3 main
//Collected 1 main
//Collected 2 main
//Collected 3 main
//Collected in 1094 ms
使用 buffer() 之后,节省了200ms,而且发送是会一次性发送完,相当于一个缓存,但这里都是在 mian线程执行,并不是并行,那如何并行呢?可以用 flowOn() 来实现:
simpleFlow6()
.flowOn(Dispatchers.Default)
//.buffer(50)
.collect {
delay(300)
println("Collected $it ${Thread.currentThread().name}")
}
//Emitting 1 DefaultDispatcher-worker-1
//Emitting 2 DefaultDispatcher-worker-1
//Emitting 3 DefaultDispatcher-worker-1
//Collected 1 main
//Collected 2 main
//Collected 3 main
//Collected in 1088 ms
conflate(),合并发射项,不对每个值进行处理。
simpleFlow6()
.conflate()
//.flowOn(Dispatchers.Default)
//.buffer(50)
.collect {
delay(300)
println("Collected $it ${Thread.currentThread().name}")
}
//Emitting 1 main
//Emitting 2 main
//Emitting 3 main
//Collected 1 main
//Collected 3 main
//Collected in 793 ms
使用 conflate(),发射了三个元素,但是第二个元素接收的时候过滤掉了。
collectLatest(),取消并重新发射最后一个值。
simpleFlow6()
.collectLatest {
delay(300)
println("Collected $it ${Thread.currentThread().name}")
}
//Emitting 1 main
//Emitting 2 main
//Emitting 3 main
//Collected 3 main
//Collected in 675 ms
操作符
转换操作符
map操作符:进行数据转换操作,包括转换发射出去的数据的类型
fun main() = runBlocking {
(1..3).asFlow()
.map { request -> performRequest(request) }
.collect { println(it) }
}
suspend fun performRequest(request: Int): String {
delay(1000)
return "response $request"
}
//response 1
//response 2
//response 3
transform操作符: 操作任意值任意次,其他转换操作符都是基于transform进行扩展的。
(1..3).asFlow()
.transform { request ->
emit("Making request $request")
emit(performRequest(request))
}.collect { println(it) }
//Making request 1
//response 1
//Making request 2
//response 2
//Making request 3
//response 3
限长操作符
take操作符:take操作符返回包含第一个计数元素的流。
fun main() = runBlocking {
numbers().take(2).collect { println(it) }
}
fun numbers() = flow<Int> {
try {
emit(1)
emit(2)
println("This line will not execute")
emit(3)
} finally {
println("Finally in numbers")
}
}
//1
//2
//Finally in numbers
末端流操作符
末端操作符是在流上用于启动流收集的挂起函数。collect 是最基础的末端操作符,但是还有另外一些更方便使用的末端操作符;
- 转化为各种集合,例如 toList 和 toSet。
- 获取第一个 (first) 值与确保流发射单个 (single) 值的操作符。
- 使用 reduce 与 fold 将流规约到单个值。
下面给一个例子:
fun main() = runBlocking {
val sum = (1..5).asFlow()
.map { it * it }
.reduce { a, b -> a + b }
println(sum)
}
//55
组合操作符
zip操作符: zip操作符用于组合两个流中的相关值。
fun main() = runBlocking {
val nums = (1..3).asFlow().onEach { delay(300) }
val strs = flowOf("One", "Two", "Three").onEach { delay(400) }
val startTime = System.currentTimeMillis()
nums.zip(strs) { a, b -> "$a -> $b" }.collect {
println("$it at ${System.currentTimeMillis() - startTime}ms from start")
}
}
//1 -> One at 424ms from start
//2 -> Two at 825ms from start
//3 -> Three at 1233ms from start
展平操作符
流表示异步接收的值序列,所以很容易遇到这样的情况:每个值都会触发对另一个值序列的请求,然而,由于流具有异步的性质,因此需要不同的展平模式,为此,存在一系列的流展平操作符:
flatMapConcat 连接模式
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
(1..3).asFlow()
.onEach { delay(100) }
.flatMapConcat { requestFlow(it) }
.collect { println("$it at ${System.currentTimeMillis() - startTime}ms from start") }
}
fun requestFlow(i: Int) = flow<String> {
emit("$i: First")
delay(500)
emit("$i: Second")
}
//1: First at 136ms from start
//1: Second at 637ms from start
//2: First at 753ms from start
//2: Second at 1259ms from start
//3: First at 1366ms from start
//3: Second at 1872ms from start
flatMapMerge 合并模式
(1..3).asFlow()
.onEach { delay(100) }
.flatMapConcat { requestFlow(it) }
.collect { println("$it at ${System.currentTimeMillis() - startTime}ms from start") }
//1: First at 158ms from start
//2: First at 260ms from start
//3: First at 368ms from start
//1: Second at 664ms from start
//2: Second at 765ms from start
//3: Second at 875ms from start
flatMapLatest 最新展平模式
(1..3).asFlow()
.onEach { delay(100) }
.flatMapLatest { requestFlow(it) }
.collect { println("$it at ${System.currentTimeMillis() - startTime}ms from start") }
//1: First at 164ms from start
//2: First at 274ms from start
//3: First at 380ms from start
//3: Second at 881ms from start
流的异常处理
当运算符中的发射器或代码抛出异常时,有几种处理异常的方法:
1. try/catch块
2. catch函数
fun main() = runBlocking {
try {
simpleFlow().collect {
println(it)
check(it <= 1) { "Collected $it"}
}
} catch (e: Throwable) {
println("Catch $e")
}
flow {
emit(1)
throw ArithmeticException("Div 0")
}.catch { e: Throwable ->
println("Catch $e")
emit(10)
}.flowOn(Dispatchers.IO)
.collect { println(it) }
}
fun simpleFlow() = flow<Int> {
for (i in 1..3) {
println("Emitting $i")
emit(i)
}
}
//Emitting 1
//1
//Emitting 2
//2
//Catch java.lang.IllegalStateException: Collected 2
//Catch java.lang.ArithmeticException: Div 0
//1
//10
流的完成
当流收集完成时(普通情况或异常情况),它可能需要执行一个动作。
命令式 finally 块:
fun main() = runBlocking {
try {
simpleFlow().collect { println(it) }
} finally {
println("Done")
}
}
fun simpleFlow() = (1..3).asFlow()
//1
//2
//3
//Done
onCompletion 声明式处理:
fun main() = runBlocking {
simpleFlow()
.onCompletion { e ->
if (e != null) println("Flow completed exception")
}.catch { e ->
println("Catch $e")
}.collect {
println(it)
}
}
fun simpleFlow() = flow<Int> {
emit(1)
throw RuntimeException()
}
//1
//Flow completed exception
//Catch java.lang.RuntimeException
Flow 实现多路复用
多数情况下,我们可以通过构造合适的 Flow 来实现多路复用的效果。
fun main() = runBlocking<Unit> {
val name = "guest"
coroutineScope {
listOf(::getUserFromLocal, ::getUserFromRemote)
.map { function ->
function.call(name)
}
.map { deferred ->
flow { emit(deferred.await()) }
}.merge().collect { user -> println(user) }
}
}
fun CoroutineScope.getUserFromLocal(name: String) = async(Dispatchers.IO) {
delay(1000)
"getUserFromLocal"
}
fun CoroutineScope.getUserFromRemote(name: String) = async(Dispatchers.IO) {
delay(2000)
"getUserFromRemote"
}
//getUserFromLocal
//getUserFromRemote
StateFlow 和 ShareFlow
可以参考郭神的文章: