withContext 切換執行緒

Prerequisites:

withContext與launch差別

  • launch:啟動一個「新協程」,不阻塞當前執行
  • withContext:不會啟動「新協程」,沿用原本的協程,切換執行緒,暫停當前協程直到完成。

withContext不會再建立協程,沿用原來的協程,但會改變執行緒。

withContext是順序執行。

launch:                 withContext:
┌─────────┐              ┌─────────┐
│ 任務A   │              │  任務A  │
└───┬─────┘              └────┬────┘
    ↓                         ↓
┌───▼─────┐              ┌────▼────┐
│ 同時執行 │              │  等待   │
│ 任務B   │              │  任務B  │
└─────────┘              └────┬────┘
                               ↓
                          ┌────▼────┐
                          │ 繼續執行│
                          │  任務A  │
                          └─────────┘

withContext執行順序:

  • 會暫停當前協程
  • 等待區塊內程式執行完成
  • 自動切回原本的執行緒
  • 返回結果
1
2
3
4
5
6
7
8
9
suspend fun fetchUserData(): User {
    // 在 IO 執行緒執行網路請求
    val userData = withContext(Dispatchers.IO) {
        // 這個區塊在 IO 執行緒執行
        apiService.getUserData()
    }
    // 自動回到原本的執行緒
    return userData
}

launch執行順序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun processData() {
    viewModelScope.launch {
        // 主執行緒執行
        showLoading()
        
        // 啟動新的協程做網路請求(不阻塞)
        val job = launch(Dispatchers.IO) {
            // 在背景執行
            val data = apiService.getData()
            
            // 切回主執行緒更新 UI
            withContext(Dispatchers.Main) {
                updateUI(data)
            }
        }
    }
}

不會阻塞當前執行

不等待完成

返回 Job 物件(可用來取消)

Dispatcher

  • Dispatchers.IO
  • Dispatchers.Default
  • Dispatchers.Main
Dispatcher 用途 執行緒數量 範例
Main UI 更新 1 textView.text = “…”
IO 網路/檔案 64+ retrofit.get()、file.read()
Default CPU 計算 CPU 核心數 list.sort()、圖片壓縮
自訂 特殊需求 自訂 資料庫操作、特殊任務
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
suspend fun showDispatchers() {
    // Main - UI 操作
    withContext(Dispatchers.Main) {
        println("UI 執行緒: ${Thread.currentThread().name}")
        // 更新 UI
    }
    
    // IO - 網路/檔案操作
    withContext(Dispatchers.IO) {
        println("IO 執行緒: ${Thread.currentThread().name}")
        // 讀取檔案、網路請求
    }
    
    // Default - CPU 密集計算
    withContext(Dispatchers.Default) {
        println("Default 執行緒: ${Thread.currentThread().name}")
        // 排序、計算、圖片處理
    }
    
    // 自訂執行緒
    withContext(newSingleThreadContext("MyThread")) {
        println("自訂執行緒: ${Thread.currentThread().name}")
    }
}

withContext 的返回值

withContext 返回它區塊內的最後一行表達式的值

情境 返回值類型 範例
簡單值 基本類型 String、Int、Boolean
物件 自訂類別 User、Post
集合 集合類型 List、Map<Int, User>
可能空值 可空類型 User?、String?
成功/失敗 sealed class Result
多個值 Pair/Triple Pair <String, Int>
沒有值 Unit Unit
  • withContext 總是有返回值 - 區塊內的最後一行
  • 返回類型自動推斷 - 根據最後一行的類型
  • 可以是任何類型 - 基本類型、物件、集合、null 等
  • 與 launch 的關鍵差別 - launch 沒有返回值
  • 實際應用 - 網路請求、資料庫操作、檔案讀寫等需要結果的操作

返回String

1
2
3
4
5
6
val result: String = withContext(Dispatchers.IO) {
    // 做一些工作...
    "Hello World"  // ← 這就是返回值
}

println(result)  // 輸出: Hello World
1
2
3
4
5
6
7
8
suspend fun getUserName(): String = withContext(Dispatchers.IO) {
    val user = api.getUser()
    user.name  // ← 返回 String
}

// 使用
val name: String = getUserName()
println("用戶名: $name")

返回Int

1
2
3
4
5
6
7
suspend fun calculateSum(): Int = withContext(Dispatchers.Default) {
    val a = 10
    val b = 20
    a + b  // ← 返回 Int
}

val sum: Int = calculateSum()  // sum = 30

返回物件

1
2
3
4
5
6
7
8
9
data class User(val id: Int, val name: String)

suspend fun fetchUser(): User = withContext(Dispatchers.IO) {
    // 從 API 取得資料
    val response = api.getUserData()
    User(response.id, response.name)  // ← 返回 User 物件
}

val user: User = fetchUser()

返回多類型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 返回 Boolean
suspend fun checkNetwork(): Boolean = withContext(Dispatchers.IO) {
    return@withContext try {
        api.ping()
        true   // ← 返回 最後一行
    } catch (e: Exception) {
        false  // ← 返回 
    }
}

// 返回 List
suspend fun getItems(): List<String> = withContext(Dispatchers.IO) {
    listOf("A", "B", "C")  // ← 返回 List
}

// 返回 Unit(不返回有用值)
suspend fun logEvent(): Unit = withContext(Dispatchers.IO) {
    println("事件記錄")  // ← 返回 Unit
    // 或寫 return@withContext Unit
}

與 launch比較

1
2
3
4
5
6
7
8
9
10
11
12
// withContext:有返回值
suspend fun getData(): String = withContext(Dispatchers.IO) {
    "資料"  // ← 可以返回值
}

// launch:沒有返回值
fun doTask() {
    viewModelScope.launch(Dispatchers.IO) {
        "資料"  // ← 這個值無法取得!
        // launch 不接受返回值
    }
}

協程作用域

因為withContext是suspend 函式,所以要在協程作用域中,才能使用withContext()函式。

Junit的coroutine scope(協程作用域)是runBlocking,只有在runBlocking協程作用域下,就能呼叫suspend()函式。

1
2
3
4
5
6
7
8
  @Test
  fun coroutine13() = runBlocking {
  	// 切換至IO執行緒
    withContext(Dispatchers.IO) {
       // 程式碼
    }
    println(result2)
  }

使用方式1

withContext是suspend函式,要放在suspend函式中。

1
2
3
4
5
6
7
8
9
10
11
  suspend fun funName() {
    withContext(Dispatchers.IO) {
      delay(1000)
      println("test")
    }
  }

  @Test
  fun coroutine14() = runBlocking {
    funName()
  }

使用方式2

withContext 放在 「等號 =」右邊,傳回值為Unit。

1
2
3
4
5
6
7
8
9
  suspend fun funName() = withContext(Dispatchers.IO) {
      delay(1000)
      println("test")
  }

  @Test
  fun coroutine14() = runBlocking {
    funName()
  }
test

使用方式3

使用withContext

1
2
3
4
5
6
7
8
9
10
11
  suspend fun funName() = coroutineScope {
    withContext(Dispatchers.IO) {
      delay(1000)
      println("test")
    }
  }

  @Test
  fun coroutine14() = runBlocking {
    funName()
  }
test

使用方式4

包在協程作用域中(coroutineScope、CoroutineScope、GlobalScope)。

1
2
3
4
5
6
7
8
9
  @Test
  fun coroutine14() = runBlocking {
    coroutineScope {
      withContext(Dispatchers.IO) {
        delay(1000)
        println("test")
      }
    }
  }

使用方式5

包在協程中(launch、async)。

1
2
3
4
5
6
7
8
9
10
  @Test
  fun coroutine14() = runBlocking {
    launch {
      withContext(Dispatchers.IO) {
        delay(1000)
        println("test")
      }
    }
    println()
  }

使用方式6

在Activity,不能在Test Case。

1
2
3
4
5
6
7
8
9
GlobalScope.launch (Dispatchers.Main) {
  // 切換至io
  withContext (Dispatchers.IO) {
    delay(1000)
    println("io")
  }
  // 切換回 main
  println("main")
}

順序執行

即便result2的delay秒數才500,但也是先執行完result1,才執行result2。

1
2
3
4
5
6
7
8
9
10
11
12
13
  @Test
  fun coroutine13() = runBlocking<Unit> {
    val result1 = withContext(Dispatchers.IO) {
      delay(1000)
      "result1"
    }
    println(result1)
    val result2 = withContext(Dispatchers.IO) {
      delay(500)
      "result2"
    }
    println(result2)
  }
result1
result2

非同步執行

非同步,我自己的記法是多個協程同時執行,不用等待其它協程執行完畢。
使用launch包住withContext,就可以變成非同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  @Test
  fun coroutine15() = runBlocking {
    launch {
      val result1 = withContext(Dispatchers.IO) {
        delay(1000)
        "result1"
      }
      println(result1)
    }

    launch {
      val result2 = withContext(Dispatchers.IO) {
        delay(500)
        "result2"
      }
      println(result2)
    }
    println()
  }
result2
result1

results matching ""

    No results matching ""