前一章把回呼地獄(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、再抓內容、最後畫畫面。差別只是這些動作可以在不同執行緒之間切換,而程式碼讀起來還是線性的。

兩個立刻可見的好處#

完成這次改寫之後,會發現兩件事:

  1. 非同步流程被寫成同步的樣子,沒有巢狀 lambda。
  2. 切換執行緒(Thread)非常容易,只要切換 Dispatcher(調度器)就行。

Coroutine 把「等待」這件事下放給語言層級,搭配 Dispatcher 處理執行緒分派,所以開發者只要用線性語法描述流程,剩下的交給執行時期的 CoroutineContext 去處理。

小結#

  • 透過 suspend 標記耗時函式、coroutineScope 建立範圍、launch 啟動 Coroutine、withContext 切換執行緒,就能完成第一個 Coroutine 程式。
  • 改寫後的程式碼讀起來像同步寫法,但實際上是非同步執行,不會阻塞主執行緒。
  • 後續會深入這些工具背後的機制:搶佔式 vs 協同式多工、stackful vs stackless、以及 Coroutine 的三大要素。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10261496