昨天對 coroutine
有了基本的認識後,今天就來實作吧!
google codelab 的範例專案使用了 MVVM 架構,雖然昨天看過了,但我們還是再看一次架構圖吧!
MainActivity
: 顯示 UI、註冊監聽器,將事件傳遞給 MainViewModel
,使其透過 LiveData
更新畫面。MainViewModel
: 負責處理點擊事件,並使用 LiveData
和 MainActivity
溝通。TitleRepository
: 向 server 請求資料,並將取回的資料儲存到 Database。解釋完架構,來進入到程式碼 :
因為專案使用的是 MVVM
,而 AndroidX 已經將 CoroutineScope
整合至 ViewModel
中,所以我們需要在 build.gradle (Module)
引用其 library :
dependencies {
...
// replace x.x.x with latest version
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}
此套件將 viewModelScope
作為 ViewModel
的 extension function,目的是讓 scope 綁定到 Dispatchers.Main
,使 coroutine
在主線程被啟動,如此一來也不用在特地切到主線程來更新畫面;最後,在 ViewModel
被清除掉時 coroutine
也會自動被取消,使我們能控制好 coroutine
的生命週期。
範例有個待實作的程式碼 updateTaps()
,功能是等待一秒後更新畫面。透過 BACKGROUND ExecutorService
讓程式碼在背景執行緒中運行,當執行到 sleep()
時會阻塞當前的執行緒;所以如果我們將BACKGROUND.submit{}
拿掉,使其在主執行緒中運行的話,畫面就會被凍結一秒。
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
用 coroutine
改寫 :
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
這段程式碼做的事情和上面的一樣,但有幾點不同 :
viewModelScope
有自己預設的調度器**Dispatchers.Main
**,表示 coroutine
會在主執行緒被啟動。coroutine
被 viewModelScope
啟動後,當程式執行到 delay(1_000)
,時,被啟動的 coroutine
會從當前的主執行緒切換到其他指定的執行緒執行任務 ,因為 delay(1_000)
屬於 suspend 函式
delay()
是 suspend 函式
,所以就算 coroutine
是在主執行緒上處理任務,但執行到 delay()
時,並不會阻塞當前的主執行緒,而是透過 Dispatcher
將目前正在做事的 coroutine
切換到其他指定的執行緒去執行 suspend 函式
,等到 suspend
結束後會才會恢復 (resume),也就是將切出去的 coroutine
在切回原來的主執行緒,繼續執行他的任務 - 更新畫面。suspend
在 Kotlin 中可視為關鍵字,表示函式必須要在 coroutine
內執行,也需要在此函式內再實現一個 suspend 函式
,因此主要是用來提醒此函式是屬於耗時函式,所以需要在 coroutine
內被執行。接著我們要使用 callback
/ coroutine
的方式向 server 取得 title,並更新畫面。
先看一下使用 callback
的版本 :
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
interface TitleRefreshCallback {
fun onCompleted()
fun onError(cause: Throwable)
}
TitleRefreshCallback
有兩個實作的方法,onCompleted()
、onError()
,若取得資料失敗會執行 onError()
,並透過 snackBar 顯示錯誤訊息;成功則是會走 onCompleted()
object: TitleRefreshCallback
是 Kotlin 中建構匿名類別的方法,在這邊用來建立一個實作TitleRefreshCallback
的物件。
接著用 coroutine 改寫 :
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
還記得前面提到的 suspend 函式
嗎? suspend 函式
需要在 coroutine
內執行,在此函式內也必須再呼叫另一個 suspend 函式
,這樣才能算是一個有意義的 suspend 函式
。
我們先在 TitleRepository.kt 內定義一個負責更新 title 的 suspend 函式
:
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
先暫時在此函式中延遲 500 毫秒。
接著回到 MainViewModel.kt, 將 refreshTitle()
改為 coroutine
版本 :
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
}
catch (e: TitleRefreshError) {
_snackBar.value = e.message
}
finally {
_spinner.value = false
}
}
}
來解釋一下上述的程式碼 :
viewModelScope.launch{}
因為我們是透過 viewModelScope.launch
來啟動 coroutine
,所以當使用者離開此畫面時, 在此 Scope 涵蓋的所有 coroutine
都會被取消,也就是不會再向 server 或本地資料庫發出請求。
除了使用
launch
啟動coroutine
之外,也可以透過其他方式像是async
來啟動,以下是使用情境 :
launch
: 用於執行後不需要返回執行結果async
: 用於執行須會有回傳結果的情況
可以依照不同的使用情境來啟動 coroutine
,也可以在 coroutine
啟動後再透過 launch 或是 async
啟動子 coroutine
。
repository.refreshTitle()
refreshTitle()
是在向 repository 請求資料,還記得這是一個 suspend 函式
吧? 所以我們會在 coroutine
內執行,並且在函式恢復 (resume) 前,也就是在函式的執行期間內都不會阻塞主線程。
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
上述程式碼主要是用來處理請求資料時在 suspend 函式
內被拋出的錯誤,在 suspend 函式
中被拋出的錯誤可以直接使用 try/catch
處理。
今天查了很多關於 coroutine 的資料,因為在不清楚原理的情況下實作起來也是懵懵懂懂的,總覺得雖然做出來了但總有一層霧包著,心裡不太舒服。但也是因為這樣就花了很多時間在找資料,雖然是照著 codelab 實作,但內容都是經過筆者消化後的,希望能一起更加了解 coroutine!