一、简介
Kotlin 协程是管理后台线程的推荐方法,可通过减少回调需求来简化代码。
简介参考:https://www.jianshu.com/p/130f3888a433
1. 本示例目标:
熟悉在应用中使用 协程 来 从网络加载数据,能够将协程集成到应用中.
熟悉有关协程的 最佳做法,以及如何针对使用协程的代码 编写测试。
(官方示例: https://developer.android.com/codelabs/kotlin-coroutines#0)
2. 预备知识:
(1) 熟悉 ViewModel、LiveData、Repository 和 Room 架构组件。
(2) 具有使用 Kotlin 语法(包括扩展函数和 lambda)的经验。
(3) 对于在 Android 上使用线程(包括主线程、后台线程和回调)有基本的了解。
3. 示例中应执行的操作
(1) 调用 使用协程编写的代码并获取结果。
(2) 使用 挂起函数 让异步代码 依序调用。
(3) 使用 launch 和 runBlocking 控制代码的执行方式。
(4) 了解使用 suspendCoroutine 将现有 API 转换为协程的技巧。
(5) 将协程与架构组件一起使用。
(6) 了解测试协程的最佳做法。
二、示例详细过程
1. 下载代码:
GitHub 代码:https://github.com/googlecodelabs/kotlin-coroutines/tree/master/coroutines-codelab
kotlin-coroutines 代码库包含两个应用模块:
(1) start: 一个使用 Android 架构组件的简单应用,将向此应用添加协程
(2) finished_code : 已添加协程的项目
2. 运行初始示例应用
打开 coroutines-codelab 项目 -> 选择 start 应用模块 -> 点击 Run 运行.
2.1 基础功能
在点按屏幕后,此初始应用会使用线程在经过短暂延迟后增加计数。
它还会从网络中提取新标题并将其显示在屏幕上(模拟读取数据).
目标是将将此应用转换为使用协程.
2.2 项目结构
此应用使用架构组件将 MainActivity 中的界面代码与 MainViewModel 的应用逻辑分隔开.
<项目架构图>
(1)MainActivity 显示界面、注册点击监听器,并且可以显示 Snackbar。
它将事件传递给 MainViewModel,并根据 MainViewModel 中的 LiveData 更新屏幕。
(2)MainViewModel 处理 onMainViewClicked 中的事件,并将使用 LiveData与 MainActivity 通信
(3)Executors 定义 BACKGROUND,,后者可以在后台线程上运行内容。
(4)TitleRepository 从网络提取结果,并将结果保存到数据库。
2.3 向项目添加协程
在 app/build.gradle 添加 协程依赖项:
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}
其中核心库 和 Android拓展库:
kotlinx-coroutines-core - 用于在 Kotlin 中使用协程的主接口
kotlinx-coroutines-android - 在协程中支持 Android 主线程
同时在 Kotlin 协程版本页面上找到协程库的最新版版本号,以替代“xxx”
协程版本:https://github.com/Kotlin/kotlinx.coroutines/releases
2.4 使用协程控制界面
2.4.1 使用 viewModelScope
AndroidX lifecycle-viewmodel-ktx 库将 CoroutineScope,
添加到已配置为启动界面相关协程的 ViewModel 中.
此库将 viewModelScope 添加为 ViewModel 类的扩展函数。
此作用域绑定到 Dispatchers.Main,并会在清除 ViewModel 后自动取消。
2.4.2 从线程切换到协程
在 MainViewModel.kt 中
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
此代码使用 BACKGROUND ExecutorService(在 util/Executor.kt 中定义)在后台线程中运行。
由于 sleep 会阻塞当前线程,因此,如果在主线程上调用它,它会导致界面冻结。
在用户点击主视图的一秒钟后,它会请求信息提示控件。
从代码中移除 BACKGROUND 并重新运行代码,就能看到这种情况。
加载旋转图标将不会显示(即使refreshTitle里设置了_spinner=true),并且所有内容都将在一秒钟后“跳到”最终状态。
使用协程 做修改:
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
此代码执行的操作相同,即等待 1 秒钟后显示信息提示控件(**会显示 加载旋转图标 **)。
不过,它们存在一些重要区别:
(1)viewModelScope.launch 将在 viewModelScope 中启动协程。
这意味着,当我们传递给 viewModelScope 的作业取消时,此作业/作用域内的所有协程都将取消。
如果用户在 delay 返回之前离开了 Activity,
那么在 ViewModel 销毁后系统调用 onCleared 时,此协程将自动取消。
(2)由于 viewModelScope 的默认调度程序为 Dispatchers.Main,因此此协程将在主线程中启动。
稍后,我们将了解如何使用不同的线程。
(3)delay 函数属于 suspend 函数。在 Android Studio 的左侧边线中。
虽然此协程在主线程上运行,但 delay 不会阻塞此线程 1 秒钟。
相反,调度程序将安排协程在一秒钟内在下一个语句中恢复。
注意:原本是的 Thread.sleep(1_000)
现在改成 delay(1_000) 则不会阻塞主线程。(kotlinx.coroutines.delay)
!!!或者指定在IO 线程则会显示 加载旋转图标 : viewModelScope.launch(Dispatchers.IO) {...}
3. 通过行为测试协程
为以上编写的代码编写测试用例.
即如何使用 kotlinx-coroutines-test 库测试在 Dispatchers.Main 上运行的协程。
打开在test 目录下的 MainViewModelTest.kt
已有如下代码:
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
其中,
(1) InstantTaskExecutorRule 是一种 JUnit 规则,用于配置 LiveData 以同步执行每项任务
(2) MainCoroutineScopeRule 是此代码库中的自定义规则,用于
将 Dispatchers.Main 配置为使用 kotlinx-coroutines-test 中的 TestCoroutineDispatcher。
这样一来,测试可以将用于测试的虚拟时钟拨快,并让代码可以使用单元测试中的 Dispatchers.Main。
在 setup 方法中,系统使用测试虚构对象创建一个新的 MainViewModel 实例。
以下是 用于控制协程的测试。
确保系统在用户点按主视图的一秒钟后更新点按计数
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
其中,
调用 advanceTimeBy(1_000),这会导致主调度程序立即执行预定在 1 秒钟后恢复的协程。‘
点击 Run ‘MainViewModelTest' , 则会提示 Test pass.