深潜Kotlin协程(二十一):Flow 生命周期函数
系列电子书:传送门
Flow 可以想象成一个管道,请求的值在一个方向上流动,而相应产生的值在另一个方向上流动。当 flow 完成或出现异常时,这些信息也会被传递,并关闭途中的中间步骤。因此,当这些值开始流动时,我们可以监听值、异常或其它特征事件(如开始或完成)。为此,我们使用了 onEach
、 onStart
、onCompletion
、onEmpty
和 catch
等方法。下面让我逐一解释这些生命周期方法。
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
没有对异常做出响应。同样的情况也发生在其他功能上,如 map
、 filter
等。只有 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 操作(如 onEach
、onStart
、onCompletion
等)的 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 生命周期函数相关推荐
- 一文看透 Kotlin 协程本质
前言 公司开启新项目了,想着准备亮一手 Kotlin 协程应用到项目中去,之前有对 Kotlin 协程的知识进行一定量的学习,以为自己理解协程了,结果--实在拿不出手! 为了更好的加深记忆和理解,更全 ...
- Kotlin 协程调度切换线程是时候解开真相了
前言 协程系列文章: 一个小故事讲明白进程.线程.Kotlin 协程到底啥关系? 少年,你可知 Kotlin 协程最初的样子? 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇) 讲真,Kotl ...
- 一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
前言 协程系列文章: 一个小故事讲明白进程.线程.Kotlin 协程到底啥关系? 少年,你可知 Kotlin 协程最初的样子? 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇) 讲真,Kotl ...
- 【Kotlin 协程】Flow 异步流 ② ( 使用 Flow 异步流持续获取不同返回值 | Flow 异步流获取返回值方式与其它方式对比 | 在 Android 中使用 Flow 异步流下载文件 )
文章目录 一.使用 Flow 异步流持续获取不同返回值 二.Flow 异步流获取返回值方式与其它方式对比 三.在 Android 中 使用 Flow 异步流下载文件 一.使用 Flow 异步流持续获取 ...
- 【Kotlin 协程】Flow 异步流 ⑤ ( 流的上下文 | 上下文保存 | 查看流发射和收集的协程 | 不能在不同协程中执行流的发射和收集操作 | 修改流发射的协程上下文 | flowOn函数 )
文章目录 一.流的上下文 1.上下文保存 2.流收集函数原型 3.流发射函数原型 4.代码示例 - 查看流发射和收集的协程 5.代码示例 - 不能在不同协程中执行相同流的发射和收集操作 二.修改流发射 ...
- 大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
前言:苟有恒,何必三更眠五更起:最无益,莫过一日曝十日寒. 前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间.这里重新行 ...
- 【Kotlin 协程】Flow 异步流 ④ ( 流的构建器函数 | flow 构建器函数 | flowOf 构建器函数 | asFlow 构建器函数 )
文章目录 一.流的构建器函数 1.flow 构建器 2.flowOf 构建器 3.asFlow 构建器 一.流的构建器函数 1.flow 构建器 在之前的博客 [Kotlin 协程]Flow 异步流 ...
- Kotlin 协程Flow主要操作符(一)
Kotlin 协程Flow主要操作符(一) 1. 主要导包 2. map 转换操作符 3. filter过滤操作符 4. take限长操作符 5. drop丢弃操作符 6. flowOn操作符 7. ...
- Kotlin 协程Flow、StateFlow、ShareFlow
Kotlin 协程Flow.StateFlow.ShareFlow 数据流 数据流以协程为基础构建,可提供多个值.从概念上来讲,数据流是可通过异步方式进行计算处理的一组数据序列.所发出值的类型必须相同 ...
最新文章
- 肖仰华:知识图谱构建的三要素、三原则和九大策略 | AI ProCon 2019
- php 中find,Linux中find命令的用法汇总
- oracle数据库函数和存储过程的包
- linux两个网段默认网关_Linux下配置多网卡多网关
- 传递参数的2种情况的理解。
- URLDecoder: Illegal hex characters in escape (%) pattern ...
- 计算机底纹不起作用,CSS - 背景颜色在IE11中不起作用(CSS - background-color not working in IE11)...
- SQL优化之not in
- ios中UIView和CALayer关系
- windows service 2008 R2 升级 sp1遇到的问题
- 8-汇编语言数据长度及寻址-bx/si/di/bp+ss+ptr+div+dd+dup
- Grafana实现参数查询功能
- 2023年进入TK海外直播公会,应该怎么玩?怎么申请?
- 用python输出圣诞树_教你怎样用Python画了一棵圣诞树,赶紧来学习
- [英语] It_be_XXX_that_YYY强调句句式
- Debian修改桌面系统
- 量子计算(1)量子力学基本理论(上)
- 浅层神经网络回归预测,基于MATLAB。 模型包括BPNN,极限学习机(ELM)和Elman网络
- 对文本文件的加密解密
- 分组交换网中的时延详解
热门文章
- 现代C++教程1X读书笔记
- 几张图片生成3D模型?距离真正的AI建模还有多远?
- html5加载更多,HTML5[7]: 实现网页版的加载更多
- 找不到设备.将计算机连接,win10系统宽带连接显示不可使用找不到设备的修复方法...
- 实现变色TextView及ViewPager指示器(原来可以这么简单)
- 发送给客户的文件,怕泄漏怎么办?
- aptx android8,支持aptxHD和LDAC!安卓8.0蓝牙音质大爆发
- python将数字转换为中文_Python:将数字转换为文字
- python 头条 sign 参数 此篇针对实时列表 请使用73版本的谷歌浏览器
- 自我检查,看清自己 看清自己什么皮肤。