【Android】Android Studio 中的协程 Android Studio 中的协程一. 概述二. 实现比赛进度2.1. 实现run方法2.2. LaunchedEffect 方法2.3. 结构化并发2.4. 协程作用域2.5 rememberCoroutineScope三. 单元测试3.1. 导入单元测试库3.2. 编写测试代码一. 概述我们这里使用一个模拟选手赛跑的demo来学洗Android Studio中的协程。点按 Start 按钮开始比赛。比赛过程中Start 按钮上的文字将变为 Pause。您可以随时使用此按钮暂停或继续比赛。比赛开始后您可以通过状态指示器查看每位选手的进度。StatusIndicator 可组合函数会显示每位选手的进度状态。该函数使用 LinearProgressIndicator 可组合项来显示进度条。您将使用协程来更新进度值。二. 实现比赛进度2.1. 实现run方法/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an AS IS BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */packagecom.example.racetracker.uiimportandroid.util.Logimportandroidx.compose.runtime.getValueimportandroidx.compose.runtime.mutableStateOfimportandroidx.compose.runtime.setValueimportkotlinx.coroutines.CancellationExceptionimportkotlinx.coroutines.delayimportkotlin.math.log/** * This class represents a state holder for race participant. */classRaceParticipant(valname:String,valmaxProgress:Int100,valprogressDelayMillis:Long500L,privatevalprogressIncrement:Int1,privatevalinitialProgress:Int0){init{require(maxProgress0){maxProgress$maxProgress; must be 0}require(progressIncrement0){progressIncrement$progressIncrement; must be 0}}/** * Indicates the race participants current progress */varcurrentProgressbymutableStateOf(initialProgress)privateset/** * Regardless of the value of [initialProgress] the reset function will reset the * [currentProgress] to 0 */funreset(){currentProgress0}suspendfunrun(){try{// 为了模拟比赛添加一个while循环在没有完成比赛前一直循环while(currentProgressmaxProgress){// 使用挂起函数模拟比赛中的不同进度间隔。 延迟 函数执行会挂起不会阻止执行delay(progressDelayMillis)currentProgressprogressIncrement;}}catch(e:CancellationException){Log.e(RaceParticipant,$name:${e.message})throwe}}}/** * The Linear progress indicator expects progress value in the range of 0-1. This property * calculate the progress factor to satisfy the indicator requirements. */valRaceParticipant.progressFactor:Floatget()currentProgress/maxProgress.toFloat()在您刚刚添加完的代码时会在 Android Studio 中调用 delay() 函数的代码左侧看到一个图标如以下屏幕截图所此图标表示挂起点相应函数可能会挂起稍后再恢复运行。在协程等待延迟时间完成的过程中主线程不会阻塞如下图所示2.2. LaunchedEffect 方法当用户按Start按钮时 你需要对每个选手实例调用run()挂起函数从而开始比赛。因此你需要启动一个协程来调用run()函数。启动协程触发比赛时 您需要针对两位参赛者确保以下方面当用户点击Start按钮时即启动协程之时他们都会立刻开始跑当用户点击Pause或Reset按钮时即取消携程之时他们分别暂停或停止奔跑当用户关闭应用时系统会妥善管理取消操作即取消所有协程并将其绑定到一个生命周期。在之前你可能了解到您只能通过另外一个挂起函数调用挂起函数。如需从可组合箱内安全地调用挂起函数您需要使用LaunchedEffect()可组合项。只要还在组合中LaunchedEffect()可组合项就会运行提供的挂起函数。您可以使用LaunchedEffect()可组合函数实现下述所有目标借助LaunchedEffect()可组合项您可以安全从可组合项调用挂起函数当LaunchedEffect()函数进入组合时它会启动一个协程并将代码块作为参数传递。只要还在组合中它就会运行提供的挂起函数。当用户点Start按钮时LaunchedEffect()会进入组合并启动一个协程来说更新进度。当LaunchedEffect()退出组合时携程会被取消。在应用中如果用户点击了Pause/Reset按钮系统会从组合中移除LaunchedEffect()并取消底层协程。对于这个例子 您无需明确提供调度程序因为LaunchedEffect()会负责。ComposablefunRaceTrackerApp(){...varraceInProgressbyremember{mutableStateOf(false)}LaunchedEffect(playerOne,playerTwo){playerOne.run()playerTwo.run()}RaceTrackerScreen(...)}当你运行项目点击Start发现选手2是在选手1完成比赛后才开始跑的很显然这并不公平 我们应该并发启动。方法参数解释2.3. 结构化并发使用协程写代码的方式称为结构化并发。这种编程方式可以提高代码的可读性并缩短开发时间。结构化并发的思路是协程具有层次结构任务可以启动子任务子任务可以再启动子任务。这种层次结构的单位称为协程作用域。协程作用域应始终与生命周期相关联。协程 API 在设计上遵循这种结构化并发机制。您不能通过未标记为“挂起”的函数调用挂起函数。此限制可确保您从协程构建器中例如 launch调用挂起函数。这些构建器反过来又会与CoroutineScope相关联。为了让两位参赛者同时开跑 你需要启动两个单独的协程并将run函数的每次调用都移动到这些携程中。使用launch构建器封装对playerOne.run()的调用。LaunchedEffect(playerOne,playerTwo){launch{playerOne.run()}launch{playerTwo.run()}raceInProgressfalse}LaunchedEffect(可变变量){// 执行逻辑}// 注意 当变量变化时 会重新执行LaunchedEffect的逻辑。如果只想要执行一次 参数可以是不可变的或者直接写成Uint。你运行项目后点击Start虽然您认为比赛会开始但是按钮的文字立即意外的变回了Start。但是我们的实际要求应该是等两位选手都跑完了才将Pause按钮的文字重置为Start。现在之所以有这个问题是因为你启动两个协程后 就立即更新了比赛的标记 实际上是等两个协程完成后再更新标记。launch构建函数会启动一个协程来执行playerOne.run()然后立即返回以执行代码块中下一行执行playerOne.run()函数的第二个launch构建函数也会发生相同的执行流程。第二个launch构建器返回后raceInProgress标记会立即更新。2.4. 协程作用域coroutineScope挂起函数会创建一个CoroutineScope并通过当前作用域调用指定的挂起代码块。该作用域会继承LaunchedEffect()作用域中的coroutineContext当指定代码块及其所有子协程完成后作用域会立即返回。对于这个应用它会在两位参赛者对象都完成run()函数的执行后立即返回。 修改代码如下:ComposablefunRaceTrackerApp(){valplayerOneremember{RaceParticipant(namePlayer 1,progressIncrement1)}valplayerTworemember{RaceParticipant(namePlayer 2,progressIncrement2)}varraceInProgressbyremember{mutableStateOf(false)}if(raceInProgress){// 增加LaunchedEffect可组合选项LaunchedEffect(playerOne,playerTwo){// coroutineScope 挂起函数会创建一个 CoroutineScope// 并通过当前作用域调用指定的挂起代码块。该作用域会继承 LaunchedEffect() 作用域中的 coroutineContext。// 当指定的代码块及其所有子携程完成后作用域会立刻返回。对于RaceTracker应用它会在coroutineScope{launch{playerOne.run()}launch{playerTwo.run()}}raceInProgressfalse}}// ..... 其他实现}其流程如下当LaunchedEffect()代码块执行时控制权转移到coroutineScope{..}代码块。coroutineScope代码块以并发方式启动两个协程然后等待它们完成执行。执行完成后raceInProgress标志会更新。coroutineScope代码块只会在代码块内的所有代码均完成执行后才会返回并继续执行。对于代码块之外的代码是否存在并发仅仅是一个实现细节。这种编码方式为并发编程提供了一种结构化的方法被称为结构并发化。若要查看协程在用户点按 Reset 按钮时如何被取消请按以下步骤操作suspendfunrun(){try{while(currentProgressmaxProgress){// 使用挂起函数模拟比赛中的不同进度间隔。 延迟 函数执行会挂起不会阻止执行delay(progressDelayMillis)currentProgressprogressIncrement;}}catch(e:CancellationException){Log.e(RaceParticipant,$name:${e.message})throwe}}当比赛开始一段时间后点击Reset按钮 会抛出如下错误。2.5 rememberCoroutineScope一个获取“作用域对象”的 Composable 函数。这个对象本身并不执行任何代码只是为你提供了一个可以在 Composable 函数体之外如 onClick 回调启动协程的“容器”。它的关键价值在于生命周期管理这个 scope 与它在组合中的调用点绑定Composable 离开组合scope 就被取消。如果需要再用户交互按钮点击时启动协程就可以使用rememberCoroutineScope在 Compose 的按钮点击或其他 UI 事件回调中标准且安全的模式是:valscoperememberCoroutineScope()// 获取与UI生命周期绑定的ScopeButton(onClick{scope.launch{// 在事件回调中启动协程// 执行你的挂起操作例如显示Snackbar、发起网络请求等}})三. 单元测试3.1. 导入单元测试库需要特别留意使用协程的单元测试代码因为其执行可能是异步的并且可能发生在多个线程中。如需在测试中调用挂起函数您必须位于协程中。由于JUnit测试函数本身并不是挂起函数因此您需要使用runTest协程构建器。此构建器是kotlinx-coroutines-test库的一部分旨在用来执行测试。构建器会在新协程中执行测试主体。注意协程不仅可以直接在测试主体中启动还可以使用 runTest 通过测试中使用的对象启动。由于runTest是kotlinx-coroutines-test库的一部分因此您需要添加其依赖项。plugins{...}android{...}dependencies{...testImplementation(org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4)}然后点击顶部通知栏的Sync Now 这将完成导入和构建。3.2. 编写测试代码找到位于测试源代码集下的RaceParticipantTest.kt文件。在raceParticipant定义之后创建一个raceParticipant_RaceStarted_ProgressUpdated()函数并为其添加Test注解以定义测试。由于测试代码块需要放在runTest构建器中因此请使用表达式语法将runTest()代码块作为测试结果返回。/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an AS IS BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */packagecom.example.racetrackerimportcom.example.racetracker.ui.RaceParticipantimportkotlinx.coroutines.ExperimentalCoroutinesApiimportkotlinx.coroutines.launchimportkotlinx.coroutines.test.advanceTimeByimportkotlinx.coroutines.test.runCurrentimportkotlinx.coroutines.test.runTestimportorg.junit.Assert.assertEqualsimportorg.junit.TestclassRaceParticipantTest{privatevalraceParticipantRaceParticipant(nameTest,maxProgress100,progressDelayMillis500L,initialProgress0,progressIncrement1)OptIn(ExperimentalCoroutinesApi::class)TestfunraceParticipant_RaceStarted_ProgressUpdated()runTest{// 添加只读的expectedProgress变量并将其设置为1valexpectedProgress1// 使用launch构建启动新携程并调用raceParticipant.run() 函数以模拟比赛开始的场景launch{raceParticipant.run()}// raceParticipant.progressDelayMillis 属性的值决定了多长时间后比赛进度才会更新// 为了在progressDelayMillis时间过后测试进度你需要再测试中添加某种形式的延迟// 使用advanceTimeBy()辅助函数将时间提前raceParticipant.progressDelayMillis的值。// advanceTimeBy() 函数有助于缩短测试执行时间advanceTimeBy(raceParticipant.progressDelayMillis)// 由于 advanceTimeBy() 不会运行原定在给定持续时间运行的任务// 因此您需要调用 runCurrent() 函数。此函数会在当前时间执行所有待处理的任务runCurrent()// 添加一个对 assertEquals() 函数的调用以检查 raceParticipant.currentProgress 属性的值// 是否与 expectedProgress 变量的值匹配以确保进度正确更新。assertEquals(expectedProgress,raceParticipant.currentProgress)}OptIn(ExperimentalCoroutinesApi::class)TestfunraceParticipant_RaceFinished_ProgressUpdated()runTest{launch{raceParticipant.run()}advanceTimeBy(raceParticipant.maxProgress*raceParticipant.progressDelayMillis)runCurrent()assertEquals(100,raceParticipant.currentProgress)}}