【Android】Kotlin Coroutines

【Android】Kotlin Coroutines

從前在處理異步的時候,會需要自己開 thread 來進行操作。而在 Kotlin 裡則可以透過 Coroutines 來處理,可以簡單地理解成 Coroutines 是拿來取代 thread 的。根據 Android Developers 官網上所提及的, Coroutines 有輕量、減少記憶體洩漏、內建支援取消及整合 Jetpack 等好處。

引入 Coroutines 套件

要在 Android 中使用 Coroutines ,首先先確定專案的 Gradle 版本是否高於 5.3 ,若是的話 Gradle 會自動幫我們處理掉;若版本低於 5.3 我們則需要先在 app 層級的 build.gradle 引入其套件:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

倒數計時

假設我們現在有個需求,需要可以倒數十秒,並且顯示在畫面上,那麼用 thread 的寫法可能會長得像這樣:

Thread {
    for(i in 10 downTo 1) {
    	Thread.sleep(1000)
        runOnUiThread { textView.text = "倒數$i秒" }
    }
    runOnUiThread { textView.text = "結束!" }
}.start()

而若用 Coroutines 來寫,可能會長得像這樣:

val job = CoroutineScope(Dispathcers.Main) {
    for(i in 10 downTo 1) {
    	delay(1000)
    	textView.text = "倒數$i秒"
    }
    textView.text = "結束!"
}

Scope

在範例的一開始可以看到 CoroutineScope 這個東西,事實上所有 Coroutines 的東西都需要在 CoroutineScope 裡面執行,我們可以透過 CoroutineScope 來追蹤、取消不需要使用的 coroutine 來避免記憶體洩漏的問題,以下列舉一些常見的 CoroutineScope

CoroutineScope: 在非 ViewModel 或 lifecycle owner 的地方使用 coroutines 時
GlobalScope: 在整個 application 的生命週期中執行 coroutines 時
LifecycleScope: 在 lifecycle owner (Activity/Fragment) 使用 coroutines 時
ViewModelScope: 在 ViewModel 中使用 coroutines 時
RunBlocking: 在 blocking code 中要執行 suspend 的 function 時使用

Job

在範例中可以看到宣告了一個叫做 job 的常數,而這個 job 常數的型態就是 Job ,其代表的就是 coroutines(及其生命週期),透過這個 Job 我們可以來追蹤 coroutines 的狀態,或是取消該 coroutines。例如在 viewModel 中呼叫 repo 的 callApi() 時,我們希望能只存在一個 callApi() 的 request,那麼我們在執行 repo.callApi() 可以先對原先的 job 進行取消:

var job: Job? = null

fun getApiData() {
    job?.cancel()
    job = viewModelScope.launch(Dispatchers.IO) {
    	repo.callApi()
    }
}

Dispatchers

在上面的範例中可以看到 Dispatchers.MainDispatchers.IO ,這個 Dispatcher 的作用是用來決定要使用哪種 thread 來執行這個 coroutine。而 Dispatcher 的類別可以分為以下:

Dispatchers.Default: 指的是預設的 dispatcher
Dispatchers.Main: 在 main thread 上執行 coroutine,如 UI 上的操作
Dispatchers.IO: 用來執行 IO 工作,例如網路相關操作
Dispatchers.Unconfined: 在當前的 thread 上執行 coroutine,但當暫停回復後可能會跑到其他 thread 上執行

Suspend

至此,我們可以基本上了解在倒數計時範例中的程式碼各代表了什麼意思。接下來我們再來說說一些其他例子。譬如現在有的讀檔的動作,同時要控制畫面上的 ProgressBar ,因此我們會需要先在 UI thread 上去操控 progressbar 讓它顯示,之後切到 IO thread 來執行讀檔的動作,讀檔完成後再次切到 UI thread 來讓 progressbar 消失,這一系列的操作在 coroutine 下我們可以寫成這樣:

lifecycleScope.launch(Dispatchers.Main) {
    progressBar.visibility = View.VISIBLE
    readFile()
    progressBar.visibility = View.GONE
}

suspend fun readFile() {
    withContext(Dispatchers.IO) {
    	// read file
    }
}

單看 lifecycleScope 裡面可能會以為這只有單純在 UI thread 上執行,但看到 readFile() 這個函式裡會發現,他透過 withContext(Dispatchers.IO) 的方式切換到了 IO thread。

而在 readFile() 函式的前面,有個 suspend 代表這個 function 是可以被暫停的,這邊暫停的意思是,把呼叫 readFile() 的 lifecycleScope 暫停,讓 UI thread 先去執行其他的事情,等 readFile() 執行完後,再恢復 lifecycleScope 的執行。

withContext()

上面提到透過 withContext() 可以達到切換 thread 的目的,實際上使用 withContext() 時,他會建立新的 scope 並暫停當前的 block ,因此才可以達到切換不同 thread 的目的,當他執行完畢後,會再返回原本被暫停的 block。

Async

有時候我們會希望執行完 coroutine 能取得一個回傳值。這時候就需要使用 async 了, async 跟 launch 一樣,都會建立一個 coroutine ,差別只在於 async 會多回傳一個 Deferred<T> 型態的物件,這個 Deferred<T> 是繼承 Job 型態,也就是說這邊指的是被擱置的工作。當 async 內的 coroutine 執行完成後,會回傳 block 中的最後一個 statement,如下則是"完成"二字:

val job = async {
    // do some coroutine
    "完成"
}

val result = job.await()

至此,我們間單的對 Coroutines 進行介紹,透過 Coroutines 我們可以更簡便的來執行一些異步的需求,並且也能讓程式碼看起來更加簡潔、易讀,重點是真的很省事,推薦大家可以使用看看。

資料來源:
Kotlin Coroutine 教學
Kotlin Coroutines 那一兩件事情
Several Types of Kotlin Coroutine Scope Difference: CoroutineScope, GlobalScope, etc.
Kotlin coroutines on Android
Kotlin / kotlinx.coroutines