Android Coroutines 協程

在 Android 開發中,主執行緒 (Main Thread) 負責更新 UI。如果你在主執行緒執行耗時操作(例如網路請求、讀寫資料庫),App 就會卡頓甚至出現 ANR (Application Not Responding)。

Coroutines (協程) 是 Kotlin 提供的非同步處理方案,它讓我們能用同步的方式寫非同步程式碼,既簡單又輕量。

核心觀念:Suspend Function

suspend 關鍵字標記的函式稱為「掛起函式」。

  • 它只能在 Coroutine 或另一個 suspend function 中呼叫。
  • 當執行到掛起點時,它會暫停目前的執行,讓出執行緒去做別的事,等到結果回來後再恢復執行。
// 定義一個耗時操作
suspend fun fetchUserData(): User {
    delay(1000) // 模擬網路延遲,不會卡住執行緒
    return User("Alice")
}

啟動 Coroutine:CoroutineScope 與 Builder

要呼叫 suspend function,我們需要建立一個 Coroutine。

launch

launch 是一個 "Fire-and-forget" 的啟動器。它會啟動一個新的 Coroutine,但不回傳結果(回傳 Job 用於控制)。

// 在 ViewModel 中
fun loadData() {
    viewModelScope.launch {
        // 這裡已經是在 Coroutine 作用域內
        val user = fetchUserData() // 執行耗時操作 (掛起)
        // 當上面執行完畢,自動回到這裡繼續執行
        updateUI(user)
    }
}

async

async 用於並發執行並等待結果。它回傳 Deferred<T> (類似 Java 的 Future 或 JS 的 Promise)。你需要呼叫 .await() 來取得結果。

viewModelScope.launch {
    // 同時發出兩個請求
    val deferredResult1 = async { fetchFromApi1() }
    val deferredResult2 = async { fetchFromApi2() }
    
    // 等待兩個都完成
    val result1 = deferredResult1.await()
    val result2 = deferredResult2.await()
    
    process(result1, result2)
}

切換執行緒:withContext (Dispatcher)

雖然 Coroutine 很聰明,但有時我們需要明確指定在哪個執行緒執行(例如大量的運算)。這時使用 Dispatchers

  • Dispatchers.Main:主執行緒 (UI 操作)。
  • Dispatchers.IO:I/O 操作 (網路、資料庫、檔案讀寫)。
  • Dispatchers.Default:CPU 密集型運算 (排序、JSON 解析)。

使用 withContext 來切換 Dispatcher:

suspend fun saveImage(bitmap: Bitmap) {
    // 切換到 IO 執行緒存檔
    withContext(Dispatchers.IO) {
        fileOutputStream.write(bitmap)
    }
    // 自動切回原本的執行緒 (例如 Main)
}

Structured Concurrency (結構化並發)

Kotlin Coroutine 強調結構化並發。這意味著新的 Coroutine 必須在一個特定的 Scope (作用域) 中啟動。

  • 當 Scope 被取消時,裡面所有的 Coroutines 也會自動被取消,避免 Memory Leak。
  • 在 Android 中,最常用的是:
    • viewModelScope:綁定 ViewModel 生命週期(推薦)。
    • lifecycleScope:綁定 Activity/Fragment 生命週期。
    • rememberCoroutineScope:在 Compose UI 中使用。
// Compose 中的範例
@Composable
fun MyScreen() {
    val scope = rememberCoroutineScope()
    
    Button(onClick = {
        // 在 click listener 中啟動 coroutine
        scope.launch {
            // 執行非同步任務
        }
    }) {
        Text("Click Me")
    }
}

小結

Coroutine 讓非同步程式碼變得像直線一樣簡單。記住三個關鍵字:

  1. suspend:掛起函式。
  2. launch / async:啟動協程。
  3. withContext:切換執行緒。

在下一章我們會繼續深入了解 Coroutine 的組態設定 CoroutineContext