- 原文地址:Easy Coroutines in Android: viewModelScope
- 原文做者:Manuel Vivo
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:twang1727
取消再也不須要的協程(coroutine)是件容易被遺漏的任務,它既枯燥又會引入大量模版代碼。viewModelScope
對結構化併發 的貢獻在於將一項擴展屬性加入到 ViewModel 類中,從而在 ViewModel 銷燬時自動地取消子協程。html
聲明:viewModelScope
將會在尚在 alpha 階段的 AndroidX Lifecycle v2.1.0 中引入。正由於在 alpha 階段,API 可能會更改,可能會有 bug。點這裏報錯。前端
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
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
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
是 viewModelScope
的默認 CoroutineDispatcher。
val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)
複製代碼
Dispatchers.Main
在此合用是由於 ViewModel 與頻繁更新的 UI 相關,而用其餘的派發器就會引入至少2個線程切換。考慮到掛起方法自身有線程封閉機制,使用其餘派發器並不合適,由於咱們不想去取代 ViewModel 已有的功能。
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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。