系列电子书:传送门


Flow 可以想象成一个管道,请求的值在一个方向上流动,而相应产生的值在另一个方向上流动。当 flow 完成或出现异常时,这些信息也会被传递,并关闭途中的中间步骤。因此,当这些值开始流动时,我们可以监听值、异常或其它特征事件(如开始或完成)。为此,我们使用了 onEachonStartonCompletiononEmptycatch 等方法。下面让我逐一解释这些生命周期方法。

onEach

为了响应每个流动的值,我们使用 onEach 函数。

suspend fun main() {flowOf(1, 2, 3, 4).onEach { print(it) }.collect() // 1234
}

onEach 的 lambda 表达式是挂起的,元素依次按顺序(顺序)被处理。因此,如果我们在 onEach 添加 delay 函数,我们将延迟每个值的流动。

suspend fun main() {flowOf(1, 2).onEach { delay(1000) }.collect { println(it) }
}
// (1 sec)
// 1
// (1 sec)
// 2

onStart

onStart 函数设置一个监听器,一旦 flow 启动,就会回调该监听器。需要注意的是, onStart 并不会等待第一个元素的响应,而是当我们请求第一个元素时,它就会被调用。

suspend fun main() {flowOf(1, 2).onEach { delay(1000) }.onStart { println("Before") }.collect { println(it) }
}
// Before
// (1 sec)
// 1
// (1 sec)
// 2

onStart(以及在 onCompletion、onEmpty、catch) 中可以发射元素,这些元素将该处往下流动。

suspend fun main() {flowOf(1, 2).onEach { delay(1000) }.onStart { emit(0) }.collect { println(it) }
}
// 0
// (1 sec)
// 1
// (1 sec)
// 2

onCompletion

有几种方法可以完成一个 flow。最常见的是在 flow 构建器完成时(比如发送了最后一个元素),尽管有可能会出现在未捕获异常或者协程取消的情况下。在所有这些情况下,我们都可以使用 onCompletion 方法为 flow 的完成添加一个监听器。

suspend fun main() = coroutineScope {flowOf(1, 2).onEach { delay(1000) }.onCompletion { println("Completed") }.collect { println(it) }
}
// (1 sec)
// 1
// (1 sec)
// 2
// Completedsuspend fun main() = coroutineScope {val job = launch {flowOf(1, 2).onEach { delay(1000) }.onCompletion { println("Completed") }.collect { println(it) }}delay(1100)job.cancel()
}
// (1 sec)
// 1
// (0.1 sec)
// Completed

在 Android 中,我们经常使用 onStart 来展示进度条(等待网络响应的指示器),之后我们使用 onCompletion 来隐藏它。

fun updateNews() {scope.launch {newsFlow().onStart { showProgressBar() }.onCompletion { hideProgressBar() }.collect { view.showNews(it) }}
}

onEmpty

flow 可能在不发射任何值的情况下完成,有可能是出现意外状况。对于这种情况,有一个 onEmpty 函数,它在 flow 完成但没有发出任何元素时会回调。我们可以使用 onEmpty 来发射一些默认值。

suspend fun main() = coroutineScope {flow<List<Int>> { delay(1000) }.onEmpty { emit(emptyList()) }.collect { println(it) }
}
// (1 sec)
// []

catch

在 flow 构建器或处理值的任何时刻,都可能发生异常。这样的异常会向下流动,关闭途中每个处理步骤;然而,它是可以被捕获和管理的。为此,我们可以使用 catch 方法。这个监听器接收异常作为参数,并允许你执行恢复操作。

class MyError : Throwable("My error")val flow = flow {emit(1)emit(2)throw MyError()
}suspend fun main(): Unit {flow.onEach { println("Got $it") }.catch { println("Caught $it") }.collect { println("Collected $it") }
}
// Got 1
// Collected 1
// Got 2
// Collected 2
// Caught MyError: My error

在上面的例子中,onEach 没有对异常做出响应。同样的情况也发生在其他功能上,如 mapfilter 等。只有 onCompletion 的处理才会被调用。

catch 方法通过捕获来阻止异常的传播。虽然前面的步骤已经完成了,但是 catch 仍然可以发射出新的值,并保持 flow 的其余部分处于活跃状态。

val flow = flow {emit("Message1")throw MyError()
}suspend fun main(): Unit {flow.catch { emit("Error") }.collect { println("Collected $it") }
}
// Collected Message1
// Collected Error

catch 只会对上游定义的函数中抛出的异常做出响应(可以想象,当下流还有异常时,仍需要捕获异常)。

在 Android 中,我们经常使用 catch 来展示 flow 中发生的异常:

fun updateNews() {scope.launch {newsFlow().catch { view.handleError(it) }.onStart { showProgressBar() }.onCompletion { hideProgressBar() }.collect { view.showNews(it) }}
}

我们也可以使用 catch 来发出默认数据以显示在屏幕上,比如空列表。

fun updateNews() {scope.launch {newsFlow().catch {view.handleError(it)emit(emptyList())}.onStart { showProgressBar() }.onCompletion { hideProgressBar() }.collect { view.showNews(it) }}
}

未捕获的异常

flow 中出现未捕获的异常则会立即取消该 flow,并且 collect 会重新抛出此异常。这种行为是挂起函数的典型行为, coroutineScope 也有同样的行为,典型的应对方法是使用 try-catch 块在 flow 的外部捕获异常:

val flow = flow {emit("Message1")throw MyError()
}suspend fun main(): Unit {try {flow.collect { println("Collected $it") }} catch (e: MyError) {println("Caught")}
}
// Collected Message1
// Caught

请注意,使用 catch 并不能防止终端操作中出现异常(因为 catch 不能用最后一个操作之后)。因此,如果 collect 中有一个异常,它将不会捕获,而是将抛出一个错误。

val flow = flow {emit("Message1")emit("Message2")
}suspend fun main(): Unit {flow.onStart { println("Before") }.catch { println("Caught $it") }.collect { throw MyError() }
}
// Before
// Exception in thread "..." MyError: My error

因此,通常的做法是将逻辑层从 collect 移动到 onEach,并将其放在 catch 之前。当我们怀疑 collect 的逻辑可能会出现异常时,这样做会特别有用。如果我们将逻辑操作从 collect 中挪走,就可以确定 catch 能捕获所有的异常。

val flow = flow {emit("Message1")emit("Message2")
}
suspend fun main(): Unit {flow.onStart { println("Before") }.onEach { throw MyError() }.catch { println("Caught $it") }.collect()
}
// Before
// Caught MyError: My error

flowOn

传给 flow 操作(如 onEachonStartonCompletion 等)的 lambda 表达式及其构建器(如 flow {..}channelFlow{..})都是挂起的。挂起函数需要有一个上下文,并且应该与它们的父协程相关联(结构化并发)。因此,你可能想知道这些函数的上下文都来自哪里。答案是:从调用的 collect 的上下文中而来。

fun usersFlow(): Flow<String> = flow {repeat(2) {val ctx = currentCoroutineContext()val name = ctx[CoroutineName]?.nameemit("User$it in $name")}
}suspend fun main() {val users = usersFlow()withContext(CoroutineName("Name1")) {users.collect { println(it) }}withContext(CoroutineName("Name2")) {users.collect { println(it) }}
}
// User0 in Name1
// User1 in Name1
// User0 in Name2
// User1 in Name2

这段代码是如何工作的? 终端操作会调用来自上游的元素,从而提供协程上下文。然而,它也可以通过 flowOn 函数进行修改(修改协程上下文):

suspend fun present(place: String, message: String) {val ctx = coroutineContextval name = ctx[CoroutineName]?.nameprintln("[$name] $message on $place")
}fun messagesFlow(): Flow<String> = flow {present("flow builder", "Message")emit("Message")
}suspend fun main() {val users = messagesFlow()withContext(CoroutineName("Name1")) {users.flowOn(CoroutineName("Name3")).onEach { present("onEach", it) }.flowOn(CoroutineName("Name2")).collect { present("collect", it) }}
}
// [Name3] Message on flow builder
// [Name2] Message on onEach
// [Name1] Message on collect

请记住, flowOn 只适用于 flow 中位于上游的函数。

launchIn

collect 是一个挂起函数,它会挂起一个协程直到 flow 完成。我们通常会使用 launch 构建器对其进行包装,以便 flow 处理可以在另一个协程上启动。为了优化这种情况,有一个 launchIn 函数,它会在作为参数传递的作用域上调用 collect

fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job =scope.launch { collect() }

launchIn 通常用来在一个单独的协程中启动一个 flow。

suspend fun main(): Unit = coroutineScope {flowOf("User1", "User2").onStart { println("Users:") }.onEach { println(it) }.launchIn(this)
}
// Users:
// User1
// User2

总结

在本章中,我们学习了不同的 flow 功能。现在我们知道如何在 flow 开始时、结束时或者在每个元素上执行某些操作;我们还知道如何捕获异常,以及如何在新的协程中启动 flow。这些都是被广泛使用的基本工具,特别是在 Android 开发中。例如,下面是一段 Android 中使用 flow 的代码:

fun updateNews() {newsFlow().onStart { showProgressBar() }.onCompletion { hideProgressBar() }.onEach { view.showNews(it) }.catch { view.handleError(it) }.launchIn(viewModelScope)
}

深潜Kotlin协程(二十一):Flow 生命周期函数相关推荐

  1. 一文看透 Kotlin 协程本质

    前言 公司开启新项目了,想着准备亮一手 Kotlin 协程应用到项目中去,之前有对 Kotlin 协程的知识进行一定量的学习,以为自己理解协程了,结果--实在拿不出手! 为了更好的加深记忆和理解,更全 ...

  2. Kotlin 协程调度切换线程是时候解开真相了

    前言 协程系列文章: 一个小故事讲明白进程.线程.Kotlin 协程到底啥关系? 少年,你可知 Kotlin 协程最初的样子? 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇) 讲真,Kotl ...

  3. 一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?

    前言 协程系列文章: 一个小故事讲明白进程.线程.Kotlin 协程到底啥关系? 少年,你可知 Kotlin 协程最初的样子? 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇) 讲真,Kotl ...

  4. 【Kotlin 协程】Flow 异步流 ② ( 使用 Flow 异步流持续获取不同返回值 | Flow 异步流获取返回值方式与其它方式对比 | 在 Android 中使用 Flow 异步流下载文件 )

    文章目录 一.使用 Flow 异步流持续获取不同返回值 二.Flow 异步流获取返回值方式与其它方式对比 三.在 Android 中 使用 Flow 异步流下载文件 一.使用 Flow 异步流持续获取 ...

  5. 【Kotlin 协程】Flow 异步流 ⑤ ( 流的上下文 | 上下文保存 | 查看流发射和收集的协程 | 不能在不同协程中执行流的发射和收集操作 | 修改流发射的协程上下文 | flowOn函数 )

    文章目录 一.流的上下文 1.上下文保存 2.流收集函数原型 3.流发射函数原型 4.代码示例 - 查看流发射和收集的协程 5.代码示例 - 不能在不同协程中执行相同流的发射和收集操作 二.修改流发射 ...

  6. 大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

    前言:苟有恒,何必三更眠五更起:最无益,莫过一日曝十日寒. 前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间.这里重新行 ...

  7. 【Kotlin 协程】Flow 异步流 ④ ( 流的构建器函数 | flow 构建器函数 | flowOf 构建器函数 | asFlow 构建器函数 )

    文章目录 一.流的构建器函数 1.flow 构建器 2.flowOf 构建器 3.asFlow 构建器 一.流的构建器函数 1.flow 构建器 在之前的博客 [Kotlin 协程]Flow 异步流 ...

  8. Kotlin 协程Flow主要操作符(一)

    Kotlin 协程Flow主要操作符(一) 1. 主要导包 2. map 转换操作符 3. filter过滤操作符 4. take限长操作符 5. drop丢弃操作符 6. flowOn操作符 7. ...

  9. Kotlin 协程Flow、StateFlow、ShareFlow

    Kotlin 协程Flow.StateFlow.ShareFlow 数据流 数据流以协程为基础构建,可提供多个值.从概念上来讲,数据流是可通过异步方式进行计算处理的一组数据序列.所发出值的类型必须相同 ...

最新文章

  1. 肖仰华:知识图谱构建的三要素、三原则和九大策略 | AI ProCon 2019
  2. php 中find,Linux中find命令的用法汇总
  3. oracle数据库函数和存储过程的包
  4. linux两个网段默认网关_Linux下配置多网卡多网关
  5. 传递参数的2种情况的理解。
  6. URLDecoder: Illegal hex characters in escape (%) pattern ...
  7. 计算机底纹不起作用,CSS - 背景颜色在IE11中不起作用(CSS - background-color not working in IE11)...
  8. SQL优化之not in
  9. ios中UIView和CALayer关系
  10. windows service 2008 R2 升级 sp1遇到的问题
  11. 8-汇编语言数据长度及寻址-bx/si/di/bp+ss+ptr+div+dd+dup
  12. Grafana实现参数查询功能
  13. 2023年进入TK海外直播公会,应该怎么玩?怎么申请?
  14. 用python输出圣诞树_教你怎样用Python画了一棵圣诞树,赶紧来学习
  15. [英语] It_be_XXX_that_YYY强调句句式
  16. Debian修改桌面系统
  17. 量子计算(1)量子力学基本理论(上)
  18. 浅层神经网络回归预测,基于MATLAB。 模型包括BPNN,极限学习机(ELM)和Elman网络
  19. 对文本文件的加密解密
  20. 分组交换网中的时延详解

热门文章

  1. 现代C++教程1X读书笔记
  2. 几张图片生成3D模型?距离真正的AI建模还有多远?
  3. html5加载更多,HTML5[7]: 实现网页版的加载更多
  4. 找不到设备.将计算机连接,win10系统宽带连接显示不可使用找不到设备的修复方法...
  5. 实现变色TextView及ViewPager指示器(原来可以这么简单)
  6. 发送给客户的文件,怕泄漏怎么办?
  7. aptx android8,支持aptxHD和LDAC!安卓8.0蓝牙音质大爆发
  8. python将数字转换为中文_Python:将数字转换为文字
  9. python 头条 sign 参数 此篇针对实时列表 请使用73版本的谷歌浏览器
  10. 自我检查,看清自己 看清自己什么皮肤。