前一章把回呼地獄(Callback Hell)與控制權轉移(Inversion of Control)兩個痛點攤開來看。這一節就動手把「登入、抓資料、畫畫面」改寫成 Coroutine(協程)版本,看看為什麼會說 Coroutine 能讓非同步程式寫得像同步程式。
加入相依#
要在專案裡使用 Coroutine,先加上對應的依賴。一般 JVM 專案:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
}Android 專案則使用整合過 Jetpack 的版本:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
}從同步寫法出發#
回顧原本的三步驟:
fun login(userName: String, password: String): Token { ... }
fun fetchLatestContents(token: Token): List<Contents> { ... }
fun showContents(contents: List<Contents>) { ... }
fun showContents() {
val token = login(userName, password)
val contents = fetchLatestContents(token)
showContents(contents)
}這段同步寫法易讀但會把主執行緒卡住,目標是保留可讀性,但不要阻塞主執行緒。
第一步:標上 suspend#
Kotlin 用 suspend 關鍵字來標記「這個函式可能會在執行中暫停」,也就是 suspend 函式(suspend function)。把耗時的兩個函式加上 suspend:
suspend fun login(userName: String, password: String): Token { ... }
suspend fun fetchLatestContents(token: Token): List<Contents> { ... }
suspend不是讓函式變成「在背景跑」,它的語意是「這個函式可能會把目前的 Coroutine 暫停一段時間,等待結果就緒再恢復」。這也是為什麼 suspend 函式只能在 CoroutineScope 中呼叫。
第二步:用 coroutineScope 包起來#
進入 Coroutine 的世界需要一個 CoroutineScope。在最外層用 coroutineScope { ... } 建立範圍,並把整段流程改成 Coroutine 風格:
suspend fun showContents() = coroutineScope {
launch {
val token = login(userName, password)
val content = fetchLatestContents(token)
withContext(Dispatchers.Main) {
showContents(content)
}
}
}幾個關鍵點:
coroutineScope { ... }建立一個新的範圍,裡面才能呼叫 suspend 函式。launch { ... }在這個範圍內啟動一個新的 Coroutine,回傳Job用來控制它。withContext(Dispatchers.Main)把後續區塊切回主執行緒,由 Dispatcher(調度器)負責排程。
第三步:理解每個改動#
把上面的改寫拆開來看:
- 加上
suspend的函式:表示這些步驟可能會暫停,等待 I/O 或其他 Coroutine 完成。 coroutineScope { ... }:建立一個結構化的範圍,內部啟動的 Coroutine 都被它管。launch { ... }:開一個新的 Coroutine 來跑非同步流程,沒有回傳值(用async才有)。withContext(Dispatchers.Main):在不離開目前 Coroutine 的情況下,臨時把區塊切到主執行緒。
寫法上看起來和同步版本幾乎一致:先取得 token、再抓內容、最後畫畫面。差別只是這些動作可以在不同執行緒之間切換,而程式碼讀起來還是線性的。
兩個立刻可見的好處#
完成這次改寫之後,會發現兩件事:
- 非同步流程被寫成同步的樣子,沒有巢狀 lambda。
- 切換執行緒(Thread)非常容易,只要切換 Dispatcher(調度器)就行。
Coroutine 把「等待」這件事下放給語言層級,搭配 Dispatcher 處理執行緒分派,所以開發者只要用線性語法描述流程,剩下的交給執行時期的 CoroutineContext 去處理。
小結#
- 透過
suspend標記耗時函式、coroutineScope建立範圍、launch啟動 Coroutine、withContext切換執行緒,就能完成第一個 Coroutine 程式。 - 改寫後的程式碼讀起來像同步寫法,但實際上是非同步執行,不會阻塞主執行緒。
- 後續會深入這些工具背後的機制:搶佔式 vs 協同式多工、stackful vs stackless、以及 Coroutine 的三大要素。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10261496