- 原文地址:Easy Coroutines in Android: viewModelScope
- 原文作者:Manuel Vivo
- 译文出自:掘金翻译计划
- 本文永久链接:github.com/xitu/gold-m…
- 译者:twang1727
Virginia Poltrack 绘图
取消不再需要的协程(coroutine)是件容易被遗漏的任务,它既枯燥又会引入大量模版代码。viewModelScope
对结构化并发 的贡献在于将一项扩展属性加入到 ViewModel 类中,从而在 ViewModel 销毁时自动地取消子协程。
声明:viewModelScope
将会在尚在 alpha 阶段的 AndroidX Lifecycle v2.1.0 中引入。正因为在 alpha 阶段,API 可能会更改,可能会有 bug。点这里报错。
ViewModel的作用域
CoroutineScope 会跟踪所有它创建的协程。因此,当你取消一个作用域的时候,所有它创建的协程也会被取消。当你在 ViewModel 中运行协程的时候这一点尤其重要。如果你的 ViewModel 即将被销毁,那么它所有的异步工作也必须被停止。否则,你将浪费资源并有可能泄漏内存。如果你觉得某项异步任务应该在 ViewModel 销毁后保留,那么这项任务应该放在应用架构的较低一层。
创建一个新作用域,并传入一个将在 onCleared()
方法中取消的 SupervisorJob,这样你就在 ViewModel 中添加了一个 CoroutineScope。此作用域中创建的协程将会在 ViewModel 使用期间一直存在。代码如下:
class MyViewModel : ViewModel() {
/**
* 这是此 ViewModel 运行的所有协程所用的任务。
* 终止这个任务将会终止此 ViewModel 开始的所有协程。
*/
private val viewModelJob = SupervisorJob()
/**
* 这是 MainViewModel 启动的所有协程的主作用域。
* 因为我们传入了 viewModelJob,你可以通过调用viewModelJob.cancel()
* 来取消所有 uiScope 启动的协程。
*/
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
/**
* 当 ViewModel 清空时取消所有协程
*/
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
/**
* 没法在主线程完成的繁重操作
*/
fun launchDataLoad() {
uiScope.launch {
sortList()
// 更新 UI
}
}
suspend fun sortList() = withContext(Dispatchers.Default) {
// 繁重任务
}
}
复制代码
当 ViewModel 销毁时后台运行的繁重操作会被取消,因为对应的协程是由这个 uiScope
启动的。
但在每个 ViewModel 中我们都要引入这么多代码,不是吗?我们其实可以用 viewModelScope
来进行简化。
viewModelScope 可以减少模版代码
AndroidX lifecycle v2.1.0 在 ViewModel 类中引入了扩展属性 viewModelScope
。它以与前一小节相同的方式管理协程。代码则缩减为:
class MyViewModel : ViewModel() {
/**
* 没法在主线程完成的繁重操作
*/
fun launchDataLoad() {
viewModelScope.launch {
sortList()
// 更新 UI
}
}
suspend fun sortList() = withContext(Dispatchers.Default) {
// 繁重任务
}
}
复制代码
所有的 CoroutineScope 创建和取消步骤都为我们准备好了。使用时只需在 build.gradle
文件导入如下依赖:
implementation “androidx.lifecycle.lifecycle-viewmodel-ktx$lifecycle_version”
复制代码
我们来看一下底层是如何实现的。
深入viewModelScope
AOSP有分享的代码。viewModelScope
是这样实现的:
private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
}
复制代码
ViewModel 类有个 ConcurrentHashSet
属性来存储任何类型的对象。CoroutineScope 就存储在这里。如果我们看下代码,getTag(JOB_KEY)
方法试图从中取回作用域。如果取回值为空,它将以前文提到的方式创建一个新的 CoroutineScope 并将其加标签存储。
当 ViewModel 被清空时,它会运行 clear()
方法进而调用如果不用 viewModelScope 我们就得重写的 onCleared()
方法。在 clear()
方法中,ViewModel 会取消 viewModelScope
中的任务。完整的 ViewModel 代码在此,但我们只会讨论大家关心的部分:
@MainThread
final void clear() {
mCleared = true;
// 因为 clear() 是 final 的,这个方法在模拟对象上仍会被调用,
// 且在这些情况下,mBagOfTags 为 null。但它总会为空,
// 因为 setTagIfAbsent 和 getTag 不是
// final 方法所以我们不用清空它。
if (mBagOfTags != null) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
onCleared();
}
复制代码
这个方法遍历所有对象并调用 closeWithRuntimeException
,此方法检查对象是否属于 Closeable
类型,如果是就关闭它。为了使作用域被 ViewModel 关闭,它应当实现 Closeable
接口。这就是为什么 viewModelScope
的类型是 CloseableCoroutineScope
,这一类型扩展了 CoroutineScope
、重写了 coroutineContext
并且实现了 Closeable
接口。
internal class CloseableCoroutineScope(
context: CoroutineContext
) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
复制代码
默认使用 Dispatchers.Main
Dispatchers.Main
是 viewModelScope
的默认 CoroutineDispatcher。
val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)
复制代码
Dispatchers.Main
在此合用是因为 ViewModel 与频繁更新的 UI 相关,而用其他的派发器就会引入至少2个线程切换。考虑到挂起方法自身有线程封闭机制,使用其他派发器并不合适,因为我们不想去取代 ViewModel 已有的功能。
单元测试 viewModelScope
Dispatchers.Main
利用 Android 的 Looper.getMainLooper()
方法在 UI 线程执行代码。这个方法在 Instrumented Android 测试中可用,在单元测试中不可用。
借用 org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version
库,调用 Dispatchers.setMain
并传入一个 singleThreadExecutor 来替换主派发器。不要用Dispatchers.Unconfined
,它会破坏使用 Dispatchers.Main
的代码的所有假设和时间线。因为单元测试应该在隔离状态下运行完好且不造成任何副作用,所以当测试完成时,你应该调用 Dispatchers.resetMain()
来清理执行器。
你可以用以下体现这一逻辑的 JUnitRule 来简化你的代码。
@ExperimentalCoroutinesApi
class CoroutinesMainDispatcherRule : TestWatcher() {
private val singleThreadExecutor = Executors.newSingleThreadExecutor()
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(singleThreadExecutor.asCoroutineDispatcher())
}
override fun finished(description: Description?) {
super.finished(description)
singleThreadExecutor.shutdownNow()
Dispatchers.resetMain()
}
}
复制代码
现在,你可以把它加入你的单元测试了。
class MainViewModelUnitTest {
@get:Rule
var coroutinesMainDispatcherRule = CoroutinesMainDispatcherRule()
@Test
fun test() {
...
}
}
复制代码
请注意这是有可能变的。TestCoroutineContext 与结构化并发集成的工作正在进行中,详细信息请看这个 issue。
如果你使用 ViewModel 和协程, 通过 viewModelScope
让框架管理生命周期吧!不用多考虑了!
Coroutines codelab 已经更新并使用它了。学习一下怎样在 Android 应用中使用协程吧。
如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。