Kennem's Blog
  • 🏠主页
  • 🔍搜索
  • 📚文章
  • ⏱时间轴
  • 🔖标签
  • 🗂️分类
  • 🙋🏻‍♂️关于
主页 » 🧩 标签

Kotlin

Kotlin协程(4)

Kotlin协程(4) Kotlin实践部分 Flow与文件下载应用 DownloadFragment.kt // DownloadFragment 是一个 Fragment 类,用于处理文件下载任务 class DownloadFragment : Fragment() { // 定义下载文件的 URL 地址,这是一个静态的常量 val URL = "https://ts1.cn.mm.bing.net/th/id/R-C.56ab5704680b6574c1b3c0a52643d8b5?rik=P8OAzrJEZS%2biuw&riu=http%3a%2f%2fjourneyz.co%2fwp-content%2fuploads%2f2019%2f09%2fGolden-Gate-Bridge.jpg&ehk=WX9eb2rUUjWBLLrsG2MQZLOMk2wtreV%2bT1Qq1tARk4s%3d&risl=&pid=ImgRaw&r=0" // 延迟初始化 mBinding,这是一个 FragmentDownloadBinding 对象,用于绑定 XML 布局文件 private val mBinding: FragmentDownloadBinding by lazy { FragmentDownloadBinding.inflate(layoutInflater) } // 重写 onCreateView 方法,在 Fragment 创建视图时调用 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // 返回绑定的根视图作为 Fragment 的界面 return mBinding.root } // 重写 onActivityCreated 方法,当与 Fragment 相关的活动的 onCreate 方法完成时调用 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // 使用 lifecycleScope 启动协程,当 Fragment 的生命周期处于 Created 状态时执行 lifecycleScope.launchWhenCreated { // 获取上下文并执行下载操作 context?.apply { // 指定下载文件的存储位置,保存在应用的外部文件目录下 val file = File(getExternalFilesDir(null)?.path, "pic.jpg") // 开始下载文件,并收集下载状态 DownloadManager.download(URL, file).collect { status -> when (status) { // 如果下载过程中有进度更新 is DownloadStatus.Progress -> { mBinding.apply { // 更新进度条和进度文本 progressBar.progress = status.value tvProgress.text = "${status.value}%" } } // 如果下载过程中出现错误 is DownloadStatus.Error -> { Toast.makeText(context, "下载错误", Toast.LENGTH_SHORT).show() } // 如果下载完成 is DownloadStatus.Done -> { mBinding.apply { // 设置进度条到 100%,并更新文本为 100% progressBar.progress = 100 tvProgress.text = "100%" } Toast.makeText(context, "下载完成", Toast.LENGTH_SHORT).show() } // 处理其他可能的下载状态 else -> { Log.d("Kennem", "下载失败") } } } } } } } DownloadManager.kt object DownloadManager { /** * 下载指定的文件并返回下载状态的流(Flow)。 * * @param url 文件下载的 URL 地址。 * @param file 下载后保存的本地文件。 * @return 下载状态的 Flow,包含下载进度、完成状态或错误信息。 */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) fun download(url: String, file: File): Flow<DownloadStatus> { return flow { // 构建 HTTP 请求对象 val request = Request.Builder().url(url).get().build() // 执行 HTTP 请求,获取响应 val response = OkHttpClient.Builder().build().newCall(request).execute() // 检查响应是否成功 if (response.isSuccessful) { response.body()!!.let { body -> // 获取内容的总长度(字节数) val total = body.contentLength() // 使用输出流将下载的数据写入本地文件 file.outputStream().use { output -> val input = body.byteStream() // 获取输入流 var emittedProgress = 0L // 记录已发出的进度 // 将输入流中的数据复制到输出流,同时跟踪复制的字节数 input.copyTo(output) { bytesCopied -> // 计算当前进度百分比 val progress = bytesCopied * 100 / total // 每当进度比上次更新多5%以上时,发出进度更新 if (progress - emittedProgress > 5) { delay(100) // 模拟网络延迟(可选) emit(DownloadStatus.Progress(progress.toInt())) // 发出进度状态 emittedProgress = progress // 更新已发出的进度 } } } } // 下载完成,发出完成状态 emit(DownloadStatus.Done(file)) } else { // 如果响应失败,抛出异常 throw IOException(response.toString()) } }.catch { // 如果发生错误,删除部分下载的文件并发出错误状态 file.delete() emit(DownloadStatus.Error(it)) }.flowOn(Dispatchers.IO) // 在 IO 线程上运行下载流程 } } DownloadStatus.kt /** * 封闭类,用于表示下载过程中的不同状态。 * 封闭类允许定义一个有限的类型集合,在使用 `when` 表达式时提供全面的类型检查。 */ sealed class DownloadStatus { /** * 表示初始状态,即尚未开始下载。 * 可以用于初始化或重置状态。 */ object None : DownloadStatus() /** * 表示正在进行的下载的进度。 * @param value 一个整数,表示当前的进度百分比(0-100)。 */ data class Progress(val value: Int) : DownloadStatus() /** * 表示下载过程中发生的错误。 * @param throwable 引发失败的异常或错误。 */ data class Error(val throwable: Throwable) : DownloadStatus() /** * 表示下载成功完成。 * @param file 下载完成的文件。 */ data class Done(val file: File) : DownloadStatus() } Flow与Room的应用 ...

2024-08-25 · 10 分钟 · 4536 字 · updated: 2024-08-27 · ShowGuan

Kotlin协程(3)

Kotlin协程(3) 操作符 过渡流操作符 可以使用操作符转换符,就像使用集合与序列一样 过渡操作符应用于上游流,并返回下游流。 这些操作符也是冷操作符,就像流一样。这类操作符本身不是挂起函数。 它运行的速度很快,返回新的转换流的定义。 transform() // 定义一个挂起函数,模拟发送请求并返回响应 suspend fun performRequest(request: Int): String { delay(1000) // 模拟网络延迟,延迟 1000 毫秒(1 秒) return "response $request" // 返回一个响应字符串 } // 测试函数,使用 JUnit 测试框架 @Test fun `test transform flow operator`() = runBlocking { // 将整数范围转换为 Flow (1..3).asFlow() .transform { request -> // 使用 transform 运算符转换流的元素 emit("Making request $request") // 发射一个字符串,表示正在发送请求 emit(performRequest(request)) // 发射 performRequest 的返回值 } .collect { v -> // 收集流的元素 println(v) // 打印收集到的元素 } } take() // 定义一个简单的 Flow,产生 Int 类型的值 fun numbers() = flow<Int> { try { emit(1) // 发射第一个整数 1 emit(2) // 发射第二个整数 2 println("This line will not execute") // 这一行代码不会被执行,因为 collect 操作会提前中止 Flow emit(3) // 发射第三个整数 3(不会执行) } finally { // 在 Flow 被收集完成或取消时执行 println("Finally in numbers") // 打印 "Finally in numbers",表示 finally 块的执行 } } // 测试函数,使用 JUnit 测试框架 @Test fun `test limit length operator`() = runBlocking { // 调用 numbers 函数,限制收集的元素数量为 2 numbers() .take(2) // 使用 take 运算符,只收集前 2 个元素 .collect { v -> // 收集 Flow 中的元素 println(v) // 打印每个收集到的元素 } } 末端操作符 末端操作符是在流上用于启动流收集的挂起函数。collect是最基础的末端操作符,但是还有另外一些更方便使用的末端操作符 转化为各种集合,例如toList和toSet 获取第一个(first)值与确保流发射单个(single)值的操作符 使用reduce与fold将流规约到单个值 组合多个流 就像Kotlin标准库中的Sequence.zip拓展函数一样,流拥有一个zip操作符用于组合两个流中的相关值 @Test fun `test zip2`() = runBlocking { // 创建一个整数流,发射 1 到 3 的数字,每个数字发射之间延迟 300 毫秒 val numbs = (1..3).asFlow().onEach { delay(300) // 模拟延迟 } // 创建一个字符串流,发射 "one"、"two" 和 "three" 的字符串,每个字符串发射之间延迟 400 毫秒 val strs = flowOf("one", "two", "three").onEach { delay(400) // 模拟延迟 } // 记录测试开始时间 val startTime = System.currentTimeMillis() // 使用 zip 操作符将两个流的元素配对 numbs.zip(strs) { a, b -> "$a -> $b" } // 将 numbs 和 strs 的每对元素组合成字符串 .collect { v -> // 收集配对后的元素 // 打印配对后的元素和从测试开始到当前时间的时间差 println("$v, consume time : ${System.currentTimeMillis() - startTime} ms ") } } 展平流 流表示异步接收的值序列,所以很容易遇到这样的情况:每个值都会触发对另一个值序列的请求,然而,由于流具有异步的性质,因此需要不同的展平模式,为此,存在一系列的流展平操作符: flatMapConcat 连接模式 flatMapMerge 合并模式 flatMapLatest 最新展平模式 // 使用 flatMapConcat 操作符的测试函数 @Test fun `test flatMapConcat`() = runBlocking<Unit> { val startTime = System.currentTimeMillis() // 记录开始时间 // 创建一个流,包含数字 1 到 3 (1..3) .asFlow() // 将数字转换为流 .onEach { delay(100) } // 在每个元素上施加 100 毫秒的延迟 // 使用 flatMapConcat 将每个元素转换为新的流并串联 .flatMapConcat { requestFlow(it) } .collect { v -> // 收集并打印每个元素的值以及从开始到现在的消耗时间 println("$v, consume time : ${System.currentTimeMillis() - startTime} ms ") } } // 使用 flatMapMerge 操作符的测试函数 @Test fun `test flatMapMerge`() = runBlocking<Unit> { val startTime = System.currentTimeMillis() // 记录开始时间 // 创建一个流,包含数字 1 到 3 (1..3) .asFlow() // 将数字转换为流 .onEach { delay(100) } // 在每个元素上施加 100 毫秒的延迟 // 使用 flatMapMerge 将每个元素转换为新的流并并发执行 .flatMapMerge { requestFlow(it) } .collect { v -> // 收集并打印每个元素的值以及从开始到现在的消耗时间 println("$v, consume time : ${System.currentTimeMillis() - startTime} ms ") } } // 使用 flatMapLatest 操作符的测试函数 @Test fun `test flatMapLatest`() = runBlocking<Unit> { val startTime = System.currentTimeMillis() // 记录开始时间 // 创建一个流,包含数字 1 到 3 (1..3) .asFlow() // 将数字转换为流 .onEach { delay(100) } // 在每个元素上施加 100 毫秒的延迟 // 使用 flatMapLatest 将每个元素转换为新的流,仅保留最新流 .flatMapLatest { requestFlow(it) } .collect { v -> // 收集并打印每个元素的值以及从开始到现在的消耗时间 println("$v, consume time : ${System.currentTimeMillis() - startTime} ms ") } } 流的异常处理 当运算符中的发射器或代码抛出异常时,有几种处理异常的方法: try/catch块 catch函数 /** * 测试用例 `test exception1`: * 使用 runBlocking 启动协程,收集 simpleFlow 的值。 * 如果值大于 1,则抛出异常并捕获。 */ @Test fun `test exception1`() = runBlocking<Unit> { try { simpleFlow().collect { v -> println(v) check(v <= 1) { "Collected $v" } } } catch (e: Throwable) { println("Caught $e") } } /** * 测试用例 `test exception2`: * 创建一个流,发射一个值后抛出异常。 * 使用 catch 操作符处理异常。 */ @Test fun `test exception2`() = runBlocking<Unit> { flow { emit(1) throw ArithmeticException("Div 0") } .catch { e: Throwable -> println("Caught: $e") } .flowOn(Dispatchers.IO) .collect { println(it) } } 流的完成 当流收集完成时(普通情况或异常情况),它可能需要执行一个动作 命令是finally块 onCompletion声明式处理 // 定义一个函数,返回1到3的值作为 Flow fun simpleFlow2() = (1..3).asFlow() @Test // 测试在 finally 块中完成流的处理 fun `test flow complete in finally`() = runBlocking { try { // 收集 simpleFlow2 流中的每个值 simpleFlow2().collect { println(it) } } finally { // 无论流的收集过程是否成功,这条消息都会在最后打印 println("Done!") } } // 定义一个函数,返回一个 Flow<Int> 类型的流 fun simpleFlow3() = flow<Int> { // 发射(emit)第一个值 emit(1) // 发射一个异常来中断流 throw RuntimeException() } @Test // 测试使用 onCompletion 操作符处理流完成 fun `test flow complete in onCompletion`() = runBlocking { simpleFlow3() // 在流完成时调用的操作符 .onCompletion { exception -> if (exception != null) { // 如果有异常,打印流以异常方式完成 println("Flow completed exceptionally") } } // 捕获流中的异常 .catch { exception -> println("Caught $exception") } // 收集 simpleFlow3 流中的每个值 .collect { println(it) } } 通道-多路复用-并发安全 Channel Channel是一个并发安全的队列,它可以用来连接协程,实现不同协程的通信。 @Test fun `test know channel`() = runBlocking<Unit> { // 创建一个整数类型的通道,用于在不同的协程之间传递数据 val channel = Channel<Int>() // 在全局范围内启动一个生产者协程,负责向通道发送数据 val producer = GlobalScope.launch { var i = 0 while (true) { // 每次发送前暂停1秒钟 delay(1000) // 递增计数器并将值发送到通道 channel.send(++i) println("send $i") } } // 在全局范围内启动一个消费者协程,负责从通道接收数据 val consumer = GlobalScope.launch { while (true) { // 接收通道中的数据 val element = channel.receive() println("receive $element") } } // 等待生产者和消费者协程结束 joinAll(producer, consumer) } Channel的容量 Channel实际上就是一个队列,队列中一定存在缓冲区,一旦这个缓冲区满了,并且一直没有人调用receive并取走函数,send就需要挂起。故意让接收端的节奏放慢,发现send总是会挂起,直到receive之后才会继续往下执行。 迭代channel produce 与 actor 构造生产者与消费者的便捷方法 可以通过produce方法启动一个生产者协程,并返回一个ReceiveChannel, 其他协程就可以用这个Channel来接收数据了。反之,可以用actor启动一个消费者协程。 @OptIn(DelicateCoroutinesApi::class) @Test fun `test fast consumer channel`() = runBlocking { // 创建一个发送通道,它通过actor协程进行数据接收和处理 val sendChannel: SendChannel<Int> = GlobalScope.actor { while (true) { // 接收并处理通道中的数据 val element = receive() println(element) } } // 在全局范围内启动一个生产者协程,负责向通道发送数据 val producer = GlobalScope.launch { for (i in 0..3) { // 向通道发送数据 sendChannel.send(i) } } // 等待生产者协程完成 producer.join() } Channel的关闭 produce和actor返回的Channel都会随着对应的协程执行完毕而关闭,也正是这样,Channel才被称为热数据流。 对于Channel,如果我们调用了它的close方法,它会立即停止接收新元素,也就是说这是它的isClosedForSend会立即返回true。而由于Channel缓冲区的存在,这时候可能还有一些元素没有被处理完,因此要等所有的元素都被读取之后isClosedForSend才会返回true。 Channel的生命周期最好由主导方来维护,建议由主导的一方实现关闭。 @Test fun `test close channel`() = runBlocking { // 创建一个无界限的整数类型的通道,用于在不同的协程之间传递数据 val channel = Channel<Int>(Channel.UNLIMITED) // 在全局范围内启动一个生产者协程,负责向通道发送数据 val producer = GlobalScope.launch { List(3) { channel.send(it) println("send $it") } // 发送完成后关闭通道 channel.close() // 打印通道的关闭状态 println( """close channel. | -ClosedForSend : ${channel.isClosedForSend} | -ClosedForReceive: ${channel.isClosedForReceive} """.trimMargin() ) } // 在全局范围内启动一个消费者协程,负责从通道接收数据 val consumer = GlobalScope.launch { for (element in channel) { println("receive $element") delay(1000) } // 消费完成后打印通道的关闭状态 println( """close channel. | -ClosedForSend : ${channel.isClosedForSend} | -ClosedForReceive: ${channel.isClosedForReceive} """.trimMargin() ) } // 等待生产者和消费者协程结束 joinAll(producer, consumer) } BroadcastChannel 发送端和接收端在Channel中存在一对多的情形,从数据处理本身来讲,虽然有多个接收端,但是同一个元素只会被一个接收端读到。广播则不然,多个接收端不存在互斥行为。 ...

2024-08-13 · 10 分钟 · 4549 字 · updated: 2024-08-13 · ShowGuan

Kotlin协程(2)

Kotlin协程(2) 异常处理的必要性 @OptIn(ExperimentalStdlibApi::class) // 标记该代码使用了实验性标准库 API @Test // 表示这是一个测试函数 fun `test CoroutineContext extend2`() = runBlocking { // 使用 runBlocking 启动协程 // 创建一个协程异常处理器,用于捕获协程中的异常 val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> println("Caught : $exception") // 打印捕获到的异常 } // 创建一个协程作用域,包含一个 Job,使用主线程调度器和异常处理器 val scope = CoroutineScope( Job() + Dispatchers.Main + coroutineExceptionHandler // Job() 用于管理协程的生命周期 ) // 在 IO 线程上启动一个新的协程 val job = scope.launch(Dispatchers.IO) { // 获取当前协程上下文中的调度器 val dispatcher = coroutineContext[CoroutineDispatcher] // 打印当前使用的调度器 println("Current dispatcher: $dispatcher") } // 等待协程完成 job.join() // 确保主协程在子协程完成后再继续 } 异常的传播 协程构建器有两种形式:1、自动传播异常(launch与actor)2、向用户暴露异常(async与produce), 当这些构建器用于创建一个根协程时(该协程不是另一个协程的子协程),前者这类构建器,异常会在它发生的第一时间被抛出,而后者则依赖用户来最终消费异常,例如通过await或receive。 // 根协程 @Test @OptIn(DelicateCoroutinesApi::class) fun `test exception propagation`() = runBlocking<Unit> { // 在 GlobalScope 中启动一个新协程,返回一个 Job 对象 val job = GlobalScope.launch { try { // 在协程中抛出一个 IndexOutOfBoundsException throw IndexOutOfBoundsException() } catch (e: Exception) { // 捕获异常并打印一条消息,指示捕获了 IndexOutOfBoundsException println("IndexOutOfBoundsException") // 如果需要可以选择打印堆栈轨迹 // e.printStackTrace() } } // 等待协程完成 job.join() // 在 GlobalScope 中启动一个新协程,并返回一个 Deferred 对象 val deffer = GlobalScope.async { // 在协程中抛出一个 ArithmeticException throw ArithmeticException() } try { // 等待异步任务完成并获取结果,如果任务抛出异常,这里会重新抛出异常 deffer.await() } catch (e: Exception) { // 捕获异常并打印一条消息,指示捕获了 ArithmeticException println("ArithmeticException") // 如果需要可以选择打印堆栈轨迹 // e.printStackTrace() } } 非根协程的异常 其他协程所创建的协程中,产生的异常总是会被传播 /* 非根协程,job是scope内的子协程,所以会直接消费掉异常 */ @Test fun `test exception propagation2`() = runBlocking { // 创建一个新的 CoroutineScope,使用 Job 作为其上下文元素 val scope = CoroutineScope(Job()) // 在 scope 中启动一个新协程,返回一个 Job 对象 val job = scope.launch { // 在协程中启动一个异步任务 async { // 在异步任务中抛出一个 IllegalArgumentException throw IllegalArgumentException() } } // 等待 launch 启动的协程完成 job.join() } 异常的传播特性 当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级。接下来,父级会进行下面几步操作: 取消它自己的子级 取消它自己 将异常传播并传递给它的父级 SupervisorJob 使用SupervisorJob时,一个子协程的运行失败不会影响其他子协程,SupervisorJob不会传播异常给它的父级,它会让子协程自己处理异常。 需求:在作用域内定义作业的UI组件,如果任何一个UI的子作业执行失败了,它并不总是有必要取消整个UI组件,但是如果UI组件被销毁了,由于它的结果不再被需要了,它就有必要使所有的子作业执行失败。 @Test fun `test supervisorJob`() = runBlocking<Unit> { // 创建一个新的 CoroutineScope,使用 SupervisorJob 作为其上下文元素 val supervisor = CoroutineScope(SupervisorJob()) // 如果使用普通的 Job,注释掉上一行并启用以下行 // val supervisor = CoroutineScope(Job()) // 在 supervisor 作用域中启动第一个子协程,返回一个 Job 对象 val job1 = supervisor.launch { // 延迟 100 毫秒以模拟一些工作 delay(100) println("child 1") // 在第一个子协程中抛出一个 IllegalArgumentException throw IllegalArgumentException() } // 在 supervisor 作用域中启动第二个子协程,返回一个 Job 对象 val job2 = supervisor.launch { try { // 无限延迟以模拟长时间运行的任务 delay(Long.MAX_VALUE) } finally { // 当协程取消时,最终会打印 "child 2 finished" println("child 2 finished") } } // 以下是可选部分,如果需要,可以取消注释 // 延迟 200 毫秒后取消 supervisor 作用域内的所有协程 // delay(200) // supervisor.cancel() // 等待 job1 和 job2 都完成 joinAll(job1, job2) } SupervisorScope 当作业自身执行失败的时候,所以子作业将会被全部取消 @Test fun `test supervisorScope`() = runBlocking<Unit> { // 使用 supervisorScope 启动一个新的协程作用域 supervisorScope { // 在 supervisorScope 中启动一个子协程,返回一个 Job 对象 launch { // 延迟 100 毫秒以模拟一些工作 delay(100) // 输出 "child 1",表示子协程已完成其任务 println("child 1") // 在协程中抛出一个 IllegalArgumentException,以测试异常传播 throw IllegalArgumentException() } // 在 supervisorScope 中启动另一个协程 try { // 延迟一个非常长的时间,模拟长时间运行的任务 delay(Long.MAX_VALUE) } finally { // 当 supervisorScope 被取消时(由于异常),最终会打印 "child 2 finished." println("child 2 finished.") } } } @Test fun `test supervisorScope2`() = runBlocking<Unit> { // 使用 supervisorScope 来启动一个协程作用域 supervisorScope { // 在 supervisorScope 中启动一个子协程 val child = launch { try { // 输出 "The child is sleeping" 表示子协程进入睡眠状态 println("The child is sleeping") // 延迟无限长的时间,模拟长时间运行的任务 delay(Long.MAX_VALUE) } finally { // 当子协程被取消时,最终会打印 "The child is cancelled" println("The child is cancelled") } } // 让出线程以确保子协程启动并进入延迟状态 yield() // 在 supervisorScope 中抛出一个异常 println("Throwing an exception from the scope") throw AssertionError() // 抛出一个断言错误,用于测试作用域的异常处理 } } 异常的捕获 使用CoroutineExcepitonHandler对协程的异常进行捕获 以下的条件被满足时,异常就会被捕获: 时机:异常是被自动抛出异常的协程所抛出的(使用launch,而不是async时); 位置:在CoroutineScope的CoroutineContext中或在一个根协程(CoroutineScope或者supervisorScope的直接子协程)中。 Android中全局异常处理 全局异常处理器可以获取到所有协程未处理的未捕获异常,不过它并不能对异常进行捕获,虽然不能阻止程序崩溃,全局异常处理器在程序调试和异常上报等场景中仍然有非常大的用处。 我们需要在classpath下面创建META-INF/services目录,并在其中创建一个名为kotlinx.coroutines.CoroutineExceptionHandler的文件,文件内容就是我们的全局异常处理器的全类名。 取消与异常 取消与异常紧密相关,协程内部使用CancellationExcepiton来进行取消,这个异常会被忽略。 当子协程被取消时,不会取消它的父协程 如果一个协程遇到了CancellationExcepiton以外的异常,它将使用该异常取消它的父协程。当父协程的所有子协程都结束后,异常才会被父协程处理。 @Test fun `test cancel and exception`() = runBlocking<Unit> { // 启动一个协程作为父协程 val job = launch { // 在父协程中启动一个子协程 val child = launch { try { // 子协程尝试延迟执行,模拟长时间运行的任务 delay(Long.MAX_VALUE) } finally { // 当子协程被取消时,执行此块 println("Child is cancelled") } } // 让父协程暂时让出线程,给其他协程执行的机会 yield() println("Cancelling child") // 取消子协程,并等待其执行完 finally 块 child.cancelAndJoin() // 再次让出线程 yield() println("Parent is not cancelled") } // 等待父协程完成执行 job.join() } @Test fun `test cancel and exception2`() = runBlocking<Unit> { // 定义一个 CoroutineExceptionHandler 来处理未捕获的异常 val handler = CoroutineExceptionHandler { _, exception -> println("Caught: $exception") } // 使用 GlobalScope 启动一个顶层协程,并应用异常处理器 val job = GlobalScope.launch(handler) { // 第一个子协程,模拟一个需要很长时间的任务 launch { try { // 使用一个很长的延迟来模拟长时间运行的任务 delay(Long.MAX_VALUE) } finally { // 确保在协程被取消时,执行以下代码 withContext(NonCancellable) { println("Children are cancelled, but exceptions are not handled") delay(100) // 确保输出语句有机会执行 println("The first child finished its non cancellable block") } } } // 第二个子协程,在短暂延迟后抛出一个异常 launch { delay(10) println("Second child throws an exception") throw ArithmeticException() // 抛出一个异常来测试异常处理 } } // 等待父协程完成 job.join() } 异常聚合 当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。在第一异常之后发生的所有其他异常,都将绑定到第一个异常之上。 @Test fun `test exception aggregation`() = runBlocking<Unit> { // 定义一个 CoroutineExceptionHandler 来处理未捕获的异常 val handler = CoroutineExceptionHandler { _, exception -> // 输出被捕获的主异常和抑制的异常(如果有) println("Caught: $exception, Suppressed: ${exception.suppressed.contentToString()}") } // 使用 GlobalScope 启动一个顶层协程,并应用异常处理器 val job = GlobalScope.launch(handler) { // 第一个子协程 launch { try { // 模拟长时间运行的任务 delay(Long.MAX_VALUE) } finally { // 在取消时抛出 ArithmeticException throw ArithmeticException() } } // 第二个子协程 launch { try { // 进行短暂的延迟 delay(100) } finally { // 在取消时抛出 IOException throw IOException() } } // 第三个子协程 launch { try { // 进行更短暂的延迟 delay(10) } finally { // 在取消时抛出 IndexOutOfBoundsException throw IndexOutOfBoundsException() } } } // 等待父协程完成,确保所有子协程的异常都被处理 job.join() } Flow-异步流 // 定义一个名为 simpleFlow 的挂起函数,返回一个 Flow<Int> suspend fun simpleFlow() = flow<Int> { // 使用 for 循环遍历从 1 到 100 的整数 for (i in 1..100) { delay(1000) // 延迟 1 秒钟,模拟异步任务的延迟 emit(i) // 向下游发射当前整数 } } // 这是一个测试函数,演示了异步、非阻塞式的行为 @Test fun `test multiple values3`() = runBlocking<Unit> { // 启动一个新的协程,不阻塞主线程 launch { // 使用 for 循环打印信息,展示主线程没有被阻塞 for (k in 1..3) { println("I am not blocked $k") delay(1500) // 延迟 1.5 秒钟 } } // 收集 simpleFlow() 发射的值 simpleFlow().collect { value -> println(value) // 打印每个收集到的值 } } 冷流 Flow是一种类似于序列的冷流,flow构建器中的代码直到流被收集的时候才运行 // 测试函数,演示 Flow 是冷流的概念 @Test fun `test flow is cold`() = runBlocking { // 创建一个新的 Flow 实例 val flow = simpleFlow2() // 首次调用 collect,开始收集 Flow 发射的值 println("Calling collect...") flow.collect { value -> println(value) // 打印每个收集到的值 } // 再次调用 collect,重新收集 Flow 发射的值 println("Calling collect again...") flow.collect { value -> println(value) // 打印每个收集到的值 } } 流的连续性 流的每次单独收集都是按顺序执行的,除非使用特殊操作符 从上游到下游每个过渡操作符都会处理每个发射出的值,然后再交给末端操作符。 // 测试函数,演示流的连续操作 @Test fun `test flow continuation`() = runBlocking { // 将 1 到 5 的整数转换成流 (1..5).asFlow() // 过滤流,只保留偶数 .filter { it % 2 == 0 } // 将每个整数转换为字符串 .map { "String $it" } // 收集流中的每个元素 .collect { value -> println("Collect $value") // 打印收集到的每个元素 } } 流构建器 flowOf构建器定义了一个发射固定值集的流。 @Test fun `test flow continuation`() = runBlocking { // 将 1 到 5 的整数转换成流 (1..5).asFlow() // 过滤流,只保留偶数 .filter { it % 2 == 0 } // 将每个整数转换为字符串 .map { "String $it" } // 收集流中的每个元素 .collect { value -> println("Collect $value") // 打印收集到的每个元素 } } 使用.asFlow()扩展函数,可以将各种集合与序列转换为流。 @Test fun `test flow builder2`() = runBlocking<Unit> { // 创建一个包含三个字符串元素的 Flow:"one"、"two" 和 "three" flowOf("one", "two", "three") // 对 Flow 中的每个元素,应用 1000 毫秒(1 秒)的延迟 .onEach { delay(1000) } // 收集 Flow 中的每个元素,并将其打印到控制台 .collect { v -> println(v) } // 将整数范围从 1 到 3 转换为 Flow 并收集每个元素 (1..3).asFlow() .collect { println(it) // 将每个收集到的整数打印到控制台 } } 流的上下文 流的收集总是在调用协程的上下文中发生,流的该属性称为上下文保存 flow{...}构建器中的代码必须遵循上下文保存属性,并且不允许从其他上下文中发射(emit) flowOn操作符,该函数用于更改流发射的上下文。 // 定义一个简单的 Flow,返回 Int 类型的值 fun simpleFlow5() = flow<Int> { // 打印当前线程的名称,表明 Flow 开始执行 println("Flow started ${Thread.currentThread().name}") // 生成从 1 到 3 的整数 for (i in 1..3) { // 延迟 1000 毫秒(1 秒)模拟耗时操作 delay(1000) // 发射(emit)当前整数 emit(i) } // 设置 Flow 在默认的调度器(Dispatcher)上运行 }.flowOn(Dispatchers.Default) // 测试函数,使用 JUnit 测试框架 @Test fun `test flow context3`() = runBlocking<Unit> { // 收集 Flow 中发射的值 simpleFlow5().collect { v -> // 打印当前线程的名称和收集到的值 println("Collected $v ${Thread.currentThread().name}") } } 启动流 使用launchIn替换collect,可以在单独的协程中启动流的收集 ...

2024-08-09 · 11 分钟 · 5211 字 · updated: 2024-08-13 · ShowGuan

Kotlin协程(1)

Kotlin协程(1) 看明白,讲清楚。 协程处理的问题: 处理耗时任务, 这种任务常常会阻塞主线程 保证主线程安全,即确保安全的从主线程调用任何suspend函数 异步任务 协程让异步逻辑同步化,杜绝回调地狱 协程最核心的点就是:函数或一段程序能够被挂起,稍后再在挂起的位置恢复 协程的挂起与恢复 常规函数基础操作包括 : invoke(call) 和 return, 协程新增了 suspend 和 resume ; suspend —— 也称为挂起或暂停,用于暂停执行当前协程,并保存所有局部变量 resume——用于让已暂停的协程从其暂停处继续执行 挂起函数 使用suspend关键字修饰的函数叫做挂起函数 挂起函数只能在协程体内或其他挂起函数内调用 Kotlin协程的两部分 基础设施层次,标准库的协程API, 主要对协程提供了概念和语义上最基本的支持 业务框架层,协程的上层框架支持 调度器 所有协程必须在调度器中运行,即使它们在主线程上运行也是如此。 ...

2024-08-06 · 10 分钟 · 4864 字 · updated: 2024-08-13 · ShowGuan

Kotlin协程(5)

Kotlin协程(5) Binding 视图绑定 View Binding: 通过View Binding, Android 会为每个XML布局文件自动生成一个绑定类。在这个类中,每个View都有一个直接对应的属性,可以避免findViewById的使用,从而减少了手动查找视图和类型转换的工作。提高代码的安全性和可读性。 val binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.textView.text = "Hello World" Data Binding: 功能比View Binding 更为强大,允许在布局文件中直接绑定UI组件到数据源(如ViewModel),这样,数据的变化会自动更新UI <data> <variable name="user" type="com.example.User"/> </data> <TextView android:text="@{user.name}"/> Activity 中: ...

2024-08-06 · 5 分钟 · 2116 字 · updated: 2024-08-13 · ShowGuan

Kotlin基础

Kotlin基础 val & var const 编译时常量只能在函数之外定义,因为编译时常量必须在编译时赋值, 而函数都是在运行时才调用, 函数内的变量也是在运行时赋值,编译时常量要在这些变量赋值前就已经存在。 函数参数 fun main(){ println(doSomething(1, false)) println(fix("Jack")) println(fix(age=10, name="Rose")) } private fun doSomething(age:Int, flag:Boolean):String{ return "result" } fun fix(name:String, age:Int = 2){ println(name + " " + age) } 函数没有返回类型时默认返回Unit ...

2024-08-03 · 11 分钟 · 5496 字 · updated: 2024-08-03 · ShowGuan
© 2025 Kennem's Blog · Powered by Hugo & PaperMod
👤 Visitors: 👀 Views: