[譯]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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索