withContext — はコルーチン内部で実行コンテキストを切り替える関数であり、指定されたコードブロックのスレッドまたはディスパッチャを一時的に変更し、結果を元のコンテキストに返します。JetBrains, 2025によると、withContextはネットワークリクエストやディスク操作で最もよく使われるコルーチンツールの一つです。この関数はブロック完了後、コルーチンが元のディスパッチャで実行を継続することを保証し、偶発的なスレッドセーフティエラーを防ぎます。
主なポイント
withContextはkotlinx.coroutinesパッケージのサスペンド関数で、指定されたCoroutineContextでコードブロックを実行し、結果を元のコンテキストに返します。関数シグネチャは次のとおりです:
suspend fun withContext (
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
contextパラメータは任意のCoroutineContextを受け入れます — 最も一般的なのは標準のDispatchers.IO、Dispatchers.Default、Dispatchers.Mainのいずれかです。ブロックはそのコンテキストで実行され、結果はwithContextが呼び出された場所に返されます。
ラムダ完了後、withContextは実行を元のディスパッチャに確実に切り替えます。つまり、開発者はバックグラウンド操作後に手動でwithContext(Dispatchers.Main)を呼び出す必要がありません — 復帰は自動的に行われます。この動作はKotlin Coroutines仕様書にバージョン1.3から記載されています。
Android開発がwithContextの主な使用分野です。典型的なシナリオ:ViewModelがメインスレッドでコルーチンを起動し、内部でネットワークリクエストのためにwithContext(Dispatchers.IO)を呼び出し、Mainへの自動復帰後、結果をUI更新に使用します。このアプローチはMVVMアーキテクチャの基盤であり、Googleの公式コルーチンガイドで推奨されています。
withContextを理解するには、CoroutineContextとその主要コンポーネントであるディスパッチャを理解する必要があります。各コルーチンはコンテキスト要素のセットを持ち、その中でディスパッチャがどのスレッドまたはスレッドプールでコードを実行するかを決定します。
| ディスパッチャ | 目的 | プールサイズ |
|---|---|---|
| Dispatchers.Main | メインUIスレッド(Android、JavaFX、Swing) | 1(メインスレッド) |
| Dispatchers.IO | ディスクおよびネットワーク操作 | 64スレッド(制限は増加) |
| Dispatchers.Default | CPU負荷の高い計算 | max(2, コア数) |
| Dispatchers.Unconfined | 固定スレッドなし | 無制限 |
withContextは新しいコルーチンを作成しないことを理解することが重要です — 既存のコルーチンのコンテキストを切り替えるだけです。これが新しいコルーチンを生成するlaunchやasyncとの主な違いです。withContextの内部実装は最適化されています:要求されたコンテキストが現在のものと一致する場合、切り替えは発生しません — 関数は同じディスパッチャで実行されます。
withContext(Dispatchers.Main)内のDispatchers.Mainは切り替えを引き起こしません — Kotlin Coroutinesはコンテキストの同一性を認識し、不要な操作をスキップします。同様に、すでにDefaultで実行中のコルーチン内のwithContext(Dispatchers.Default)はオーバーヘッドを発生させません。この最適化はContinuationInterceptorで実装されています。
初心者はwithContextとlaunch、asyncを混同しがちです。なぜなら3つの関数すべてがコルーチンとコンテキストを扱うからです。しかし、それらの目的は根本的に異なります。
| 特性 | withContext | launch | async |
|---|---|---|---|
| 新しいコルーチンを作成 | しない | する | する |
| 結果を返す | はい(Tを直接) | いいえ(Job) | はい(Deferred<T>) |
| 実行 | 逐次 | 並列 | 並列 |
| 結果の待機 | 自動 | join() | await() |
| 典型的なユースケース | ディスパッチャ切替 | ファイア・アンド・フォーゲット | 並列計算 |
バックグラウンドスレッドで1つの操作を実行し結果を得る必要がある場合 — withContextを使用します。複数の独立した操作を並列実行する必要がある場合 — awaitと共にasyncを使用します。結果が不要な場合(ロギング、キャッシュ書き込み) — launchを使用します。GoogleはAndroidアーキテクチャのRepository層でwithContextを推奨ツールとして推奨しています。
Kotlin AndroidアプリケーションでのwithContextの3つの実践的シナリオを見てみましょう。各例は特定のタスクと正しいパターンを示しています。
ViewModelがMainのコルーチンからリポジトリメソッドを呼び出します。内部でwithContext(Dispatchers.IO)がHTTPリクエストを実行し、結果が自動的に返されます:
class UserRepository(
private val api: UserApi
) {
suspend fun getUser(id: String): User {
return withContext(Dispatchers.IO) {
api.fetchUser(id)
}
}
}
ViewModel内のコルーチンはgetUserを通常のサスペンド関数と同様に呼び出します — ディスパッチャを明示的に指定せずに。withContextがスレッド切替の詳細を隠蔽します。
複数のIO操作を連続して実行する必要がある場合、withContextはそれらを1つのブロックにまとめます。これは各操作を個別のwithContextでラップするよりも効率的です:
suspend fun loadUserProfile(id: String): Profile {
return withContext(Dispatchers.IO) {
val user = api.fetchUser(id)
val posts = api.fetchPosts(id)
Profile(user, posts)
}
}
両方の操作はDispatchers.IOで実行され、Profile結果は不要なコンテキスト切替なしで作成・返却されます。操作が独立している場合は、並列実行にasyncを使用する方が良いでしょう。
場合によっては、キャンセルできないコードを実行する必要があります — 例えば、画面を閉じる際の状態保存など。withContext + NonCancellableの組み合わせがこの課題を解決します:
withContext(Dispatchers.IO + NonCancellable) {
cache.saveState(state)
analytics.logEvent("state_saved")
}
+演算子は2つのコンテキスト要素(IOディスパッチャとNonCancellableフラグ)を結合します。親コルーチンがキャンセルされてもブロックは実行されます — 終了処理に便利です。
withContextの内部実装はContinuationメカニズムに基づいています — Kotlinコルーチンの中心的な抽象化です。各サスペンドポイントは実行状態をContinuationオブジェクトに保存し、withContextも例外ではありません。
KotlinコンパイラはwithContextをkotlinx.coroutinesのwithContextメソッド呼び出しに変換し、内部で新しいDispatchedContinuationインスタンスを作成します。このオブジェクトは元のContinuationをラップし、そのディスパッチャを置き換えます。新しいディスパッチャが現在のものと異なる場合、実行は一時停止され、ブロックは対応するスレッドプールに送られ、完了後 — 元のコンテキストで再開されます。
コルーチンがすでに実行中のディスパッチャと同じものでwithContextが呼び出された場合、Kotlinはfast-pathを有効にします:ブロックは同期実行され、DispatchedContinuationの作成やスレッドプールへの送信は行われません。これにより、withContextは同じコンテキストでの繰り返し呼び出しで実質的にコストフリーになります。JetBrainsのベンチマーク(kotlinx.coroutines 1.8)によると、fast-pathは0.1 µs未満で完了します。
異なるディスパッチャによるwithContextの呼び出しごとに新しいDispatchedContinuationが作成され、スレッド切替が必要になります — 負荷に応じて1〜5 µsかかります。ほとんどのアプリケーションではこの遅延は目立ちませんが、数千回の反復があるループ内では、操作を1つのwithContextブロックに集約する価値があります。
経験豊富な開発者でもwithContextの使用時に間違いを犯します。4つの最も一般的な問題とその防止方法を見てみましょう。
開発者はしばしば操作を1つのブロックにまとめる代わりに、各行を個別のwithContextでラップします。異なるディスパッチャによる余分な呼び出しはオーバーヘッドを生み出します。
正しい方法: 逐次IO操作を1つのwithContext(Dispatchers.IO) { ... }にまとめます。一部の操作がCPU負荷の高い場合は — 同じブロック内でwithContext(Dispatchers.Default)を使用します。
withContextはコードを逐次実行します。2つの独立したネットワークリクエストが1つのwithContextにラップされている場合、それらは順次実行されます。並列処理にはasync + awaitを使用します。
// 逐次 — 遅い
withContext(Dispatchers.IO) {
val a = api.fetchA()
val b = api.fetchB()
}
// 並列 — 速い
coroutineScope {
val a = async { api.fetchA() }
val b = async { api.fetchB() }
println("${a.await()} ${b.await()}")
}
withContext実行中にコルーチンがキャンセルされると、Dispatchers.IO上のブロックも中断されます。絶対に完了しなければならない操作(データベース書き込み、分析情報送信)では、withContextをNonCancellableと組み合わせて使用します。
withContext(Dispatchers.IO)内でViewコンポーネントを決して更新しないでください。withContextはブロック全体が完了するまでMainに戻りません。UI更新はwithContextの閉じ括弧の後に配置します — そうすればコルーチンは既にメインスレッドにあります。
よくある質問
withContextはスレッドをブロックせず、既存のコルーチン内でコンテキストを切り替えるサスペンド関数です。runBlockingはコルーチンと通常コードの橋渡しで、完了まで現在のスレッドをブロックします。withContextはUIスレッドに対して安全ですが、runBlockingはそうではありません。
いいえ、withContextはsuspend関数であるため、別のsuspend関数またはコルーチン(launch/async)からのみ呼び出せます。通常の関数からはwithContextを呼び出せません — そのためにはrunBlockingまたはCoroutineScopeが必要です。
Kotlinはfast-pathを有効にします — ブロックは同じスレッドで切替なしに同期的に実行されます。オーバーヘッドは0.1 µs未満です。これはエラーではありませんが、そのような呼び出しは冗長です — withContextなしでコードを実行する方が良いでしょう。
withContext内の例外は通常のコードと同様に伝播します — try-catchを通じて。ブロックが例外をスローした場合、親コルーチンに伝播し、処理されなければそれをキャンセルします。withContext内部またはその周囲でtry-catchを使用してください。
いいえ、withContextは新しいコルーチンを作成しません。既存のコルーチンを使用しますが、そのコンテキストを一時的に変更します。これが子コルーチンを生成するlaunchやasyncとの違いです。この動作はkotlinx.coroutinesのソースコードで確認できます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。