[译]Android中的简易协程:viewModelScope

Virginia Poltrack 绘图

取消再也不须要的协程(coroutine)是件容易被遗漏的任务,它既枯燥又会引入大量模版代码。viewModelScope结构化并发 的贡献在于将一项扩展属性加入到 ViewModel 类中,从而在 ViewModel 销毁时自动地取消子协程。html

声明viewModelScope 将会在尚在 alpha 阶段的 AndroidX Lifecycle v2.1.0 中引入。正由于在 alpha 阶段,API 可能会更改,可能会有 bug。点这里报错。前端

ViewModel的做用域

CoroutineScope 会跟踪全部它建立的协程。所以,当你取消一个做用域的时候,全部它建立的协程也会被取消。当你在 ViewModel 中运行协程的时候这一点尤为重要。若是你的 ViewModel 即将被销毁,那么它全部的异步工做也必须被中止。不然,你将浪费资源并有可能泄漏内存。若是你以为某项异步任务应该在 ViewModel 销毁后保留,那么这项任务应该放在应用架构的较低一层。java

建立一个新做用域,并传入一个将在 onCleared() 方法中取消的 SupervisorJob,这样你就在 ViewModel 中添加了一个 CoroutineScope。此做用域中建立的协程将会在 ViewModel 使用期间一直存在。代码以下:android

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 启动的。ios

但在每一个 ViewModel 中咱们都要引入这么多代码,不是吗?咱们其实能够用 viewModelScope 来进行简化。git

viewModelScope 能够减小模版代码

AndroidX lifecycle v2.1.0 在 ViewModel 类中引入了扩展属性 viewModelScope。它以与前一小节相同的方式管理协程。代码则缩减为:github

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复制代码

咱们来看一下底层是如何实现的。bash

深刻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.MainviewModelScope 的默认 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 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索