Contents
Kotlin Coroutines の基礎と Kotlin 1.9 / 2.0 での主な変更点
本稿は Android 開発者を対象に、Coroutines の基本概念 と Kotlin 1.9/Kotlin 2.0(プレビュー) における実装上のポイントを公式情報に基づいて解説します。
参考リンクはすべて公式ドキュメント・ブログ記事です。
suspend 関数と CoroutineScope の本質
| 要素 | 説明 |
|---|---|
| suspend | 関数呼び出しを中断(await)できることをコンパイラに指示します。実行時には Continuation<T> が生成され、現在のコルーチンコンテキストが保存されます。この仕組みのおかげでスレッドをブロックせずに非同期処理を書けます。 |
| CoroutineScope | コルーチンの ライフサイクル と コンテキスト(Dispatcher・Job など) を保持します。scope.launch { … } のようにスコープを明示することで、キャンセルや例外伝搬が一元管理できます。 |
実装例
|
1 2 3 4 5 6 |
// Repository 側の suspend 関数 suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) { // IO Dispatcher へ切り替えてブロッキング I/O を実行 api.getUser(id) } |
withContext により、IO スレッドプールでネットワーク呼び出しを行い、結果は元のコンテキスト(通常は Main)に戻ります。
Job と Structured Concurrency の役割
| 用語 | 意味 |
|---|---|
| Job | コルーチンの実行単位。cancel() で子コルーチンへキャンセルが伝搬します。 |
| Structured Concurrency | 親スコープが終了したら必ず子ジョブも終了させる設計原則。Android の画面遷移や ViewModel の破棄に対して安全なリソース管理を実現します。 |
Kotlin 1.9 での変更点(正確な表現)
runBlockingは 常にキャンセル可能 でしたが、Kotlin 1.9 ではテスト向け API が統一され、runTest { … }に置き換えられました。runBlocking自体の挙動は変わっていません(※公式リリースノート参照)。
実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class UserViewModel( private val repository: UserRepository ) : ViewModel() { // SupervisorJob で例外が子に波及しない構造を作る private val vmScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) fun loadData() { vmScope.launch { try { val user = async { repository.fetchUser("123") }.await() _state.value = UiState.Success(user) } catch (e: IOException) { _state.value = UiState.Error(e.localizedMessage ?: "Network error") } } } override fun onCleared() { super.onCleared() vmScope.cancel() // ViewModel が破棄されたら全ジョブをキャンセル } } |
SupervisorJob により、子コルーチンの例外が他の子に影響しません。
launch と async の使い分け
| API | 戻り値 | 主な用途 |
|---|---|---|
launch |
Job | 副作用だけを行う(UI 更新、ログ出力、データ保存など) |
async |
Deferredawait() で取得 |
計算結果や外部 API のレスポンスなど 戻り値が必要 なケース |
launch の例
|
1 2 3 4 5 6 7 |
viewModelScope.launch { // ディスク書き込みは IO Dispatcher に切替えて実行 withContext(Dispatchers.IO) { repository.save(item) } // UI への通知は Main 上で安全に実行 toastMessage.postValue("保存完了") } |
戻り値を待たないので、処理が完了したかどうかを個別にチェックする必要はありません。
async / await の例(例外伝搬も同時にハンドリング)
|
1 2 3 4 5 6 7 8 9 10 |
viewModelScope.launch { try { val user = async { repository.fetchUser("123") }.await() val posts = async { repository.fetchPosts(user.id) }.await() _uiState.value = UiState.Success(user, posts) } catch (e: IOException) { _uiState.value = UiState.Error(e.localizedMessage ?: "Network error") } } |
await() 時に例外が再スローされるため、上位の try/catch で一括処理できます。
Dispatchers と withContext の選択基準
| Dispatcher | 主な用途 |
|---|---|
| Dispatchers.Main | UI スレッド上での描画・LiveData 更新など。AndroidX lifecycle-runtime-ktx が提供する Main dispatcher を使用します。 |
| Dispatchers.IO | ファイル I/O、データベースアクセス、ネットワーク呼び出しなどブロッキング系処理に最適化されたスレッドプールです。 |
| Dispatchers.Default | CPU 集中型(画像加工、暗号化、複雑な計算)に対してコア数に合わせた固定サイズプールを提供します。 |
正しい情報への訂正
- Kotlin 2.0 ベータで
Dispatchers.IOが WorkManager と連携するオプションが追加 という記述は事実ではありません。現在(2024‑12 時点)でもDispatchers.IOは純粋なスレッドプールであり、WorkManager との自動統合機能は提供されていません。 - WorkManager と Coroutines を組み合わせる場合は、
CoroutineWorkerのdoWork()内でwithContext(Dispatchers.IO)等を使用します(公式ブログ参照)。
参考: Kotlin 2.0 Preview – Coroutines
参考: WorkManager + Coroutines – official guide
withContext を使ったスレッド切り替え例
|
1 2 3 4 5 |
suspend fun processImage(uri: Uri): Bitmap = withContext(Dispatchers.Default) { // CPU 集中型処理は Default に任せることで UI が止まらない ImageProcessor.decode(uri) } |
ベストプラクティスまとめ
- UI だけ を扱うなら
Dispatchers.Main(デフォルト)。 - ブロッキング I/O は必ず
withContext(Dispatchers.IO)に包む。 - CPU 重い計算 は
Dispatchers.Defaultへ委譲する。 - スコープ全体を変更しない ように、局所的な切り替えは常に
withContextを使用する。
ViewModel・lifecycleScope で安全に UI スレッド操作
viewModelScope の特徴
- AndroidX Lifecycle が提供する
viewModelScopeは ViewModel のライフサイクル にバインドされたCoroutineScope。 - Activity/Fragment が破棄されても ViewModel が残るケース(例: 画面回転)で、ジョブは自動的にキャンセルされます。
実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class UserViewModel( private val repository: UserRepository ) : ViewModel() { private val _state = MutableLiveData<UserUiState>() val state: LiveData<UserUiState> = _state fun loadUser(id: String) { viewModelScope.launch { _state.value = UserUiState.Loading try { // suspend 関数なので withContext は不要(内部で IO Dispatcher が使用されている想定) val user = repository.getUser(id) _state.value = UserUiState.Success(user) } catch (e: Exception) { _state.value = UserUiState.Error(e.localizedMessage ?: "不明なエラー") } } } } |
viewModelScope.launch が自動的に Dispatchers.Main.immediate を使用し、UI 更新は安全です。
Retrofit の suspend 関数とエラーハンドリング
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
interface ApiService { @GET("users/{id}") suspend fun getUser(@Path("id") id: String): UserResponse } // Repository 側(Result 型で成功/失敗を統一) class UserRepository(private val api: ApiService) { suspend fun fetchUser(id: String): Result<User> = try { val resp = api.getUser(id) if (resp.isSuccessful && resp.body() != null) { Result.success(resp.body()!!) } else { Result.failure(HttpException(resp)) } } catch (e: IOException) { Result.failure(e) // ネットワーク切断等 } } |
suspend 関数は内部で Call.await() と同等の処理を行い、例外は通常の try/catch で捕捉できます。
キャンセル・タイムアウト・テスト戦略
キャンセルと timeout 系 API
| 関数 | 動作 |
|---|---|
Job.cancel() |
子ジョブへキャンセルが伝搬し、CancellationException が投げられる。 |
withTimeout(ms) |
指定時間内に完了しなければ TimeoutCancellationException をスローして処理を中断。 |
withTimeoutOrNull(ms) |
タイムアウト時に null を返すので例外処理が不要になるケースで便利。 |
Kotlin 1.9 の最適化
withTimeoutOrNullとwithTimeoutは内部実装が軽量化され、キャンセルコストが低減されています(公式リリースノート参照)。
|
1 2 3 4 5 6 7 8 9 10 11 |
viewModelScope.launch { try { val data = withTimeout(5000L) { // 5 秒でタイムアウト repository.fetchHeavyData() } _state.value = UiState.Data(data) } catch (e: TimeoutCancellationException) { _state.value = UiState.Error("処理が時間切れです") } } |
コルーチンテストのベストプラクティス
- runTest(
kotlinx-coroutines-test)を使い、仮想時間でdelay,withTimeoutなどを高速に検証。 - Turbine(
app.cash.turbine)は Flow のシーケンステストを直感的に記述でき、awaitItem()/ensureAllEventsConsumed()が利用可能。
実装例(JUnit5 + runTest)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
@OptIn(ExperimentalCoroutinesApi::class) class UserViewModelTest { private val repository = mock<UserRepository>() private lateinit var viewModel: UserViewModel @BeforeEach fun setUp() { viewModel = UserViewModel(repository) } @Test fun `loadUser success updates state`() = runTest { // Arrange whenever(repository.getUser("123")).thenReturn( Result.success(User(id = "123", name = "Alice")) ) // Act viewModel.loadUser("123") // Assert assertEquals( UserUiState.Success(User(id = "123", name = "Alice")), viewModel.state.getOrAwaitValue() ) } @Test fun `loadUser timeout produces error`() = runTest { whenever(repository.getUser(any())).thenAnswer { delay(6000) // 6 秒待機 → withTimeout(5_000) が発火 Result.success(User(id = "999", name = "Late")) } viewModel.loadUser("123") assertTrue(viewModel.state.value is UserUiState.Error) } } |
Flow のテスト例(Turbine)
|
1 2 3 4 5 6 7 8 9 10 |
@Test fun `userFlow emits loading then data`() = runTest { val flow = userRepository.userFlow() // Flow<User> flow.test { // Turbine 拡張関数 assertEquals(UserUiState.Loading, awaitItem()) assertEquals(expectedUser, awaitItem()) cancelAndIgnoreRemainingEvents() } } |
参考:
kotlinx.coroutines.test – runTest
Turbine – Flow testing library
Kotlin 2.0(プレビュー)での注目変更点とベストプラクティス
| 変更点 | 内容・影響 |
|---|---|
| Structured Concurrency の強化 | スコープ外から cancel() が呼ばれた場合でも、子ジョブが必ずキャンセルされるように内部チェックが追加されました。これにより「スコープ漏れ」バグの検出が容易になります(Kotlin 2.0 blog)。 |
| CancellationException の階層化 | CancellationException が TimeoutCancellationException から派生し、isActive チェックが高速化されました。開発者側でのコード変更は不要です。 |
| runTest の統合 | 従来の runBlockingTest と runTest が一本化され、テスト時に自動的にディスパッチャーを切り替える仕組みがシンプルになりました(Release notes)。 |
新しい SupervisorScope のデフォルト挙動 |
supervisorScope { … } 内の子コルーチンは例外が起きても他の子に影響しません。これが「UI だけ失敗してもバックグラウンド処理を続行」したいシナリオで便利です。 |
推奨パターン(Kotlin 2.0 プレビュー対応)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class DashboardViewModel( private val repo: DashboardRepository ) : ViewModel() { // 2.0 の Structured Concurrency 強化を活かす private val vmScope = viewModelScope + SupervisorJob() fun refresh() { vmScope.launch { supervisorScope { // 子の失敗が他に波及しない launch(Dispatchers.IO) { repo.fetchStatistics() } launch(Dispatchers.IO) { repo.fetchRecentActivities() } // 例外は個別にハンドリング可能 }.joinAll() _uiState.value = UiState.Refreshed } } } |
supervisorScope と SupervisorJob の組み合わせで、部分的な失敗 が全体のキャンセルにつながらない安全設計が実現できます。
まとめ:安定した Coroutines 実装に向けて
- Dispatcher は用途別に明示的に選択し、
withContextで局所的に切り替える。 - Structured Concurrency と Job の階層管理を徹底し、ViewModel・lifecycleScope にバインドしたスコープだけを使用する。
launchは副作用、asyncは結果取得と例外伝搬に使い分ける。- キャンセルとタイムアウトは必ず組み込むことでリソースリークを防止。
- テストは runTest + Turbine で決め打ちし、仮想時間による高速かつ決定的な検証を行う。
- Kotlin 2.0(プレビュー)では Structured Concurrency の強化と テスト API 統合が最大のメリットになるため、コードベースに早めに取り込むことを推奨する。
以上が Android 開発者向けに整理した Coroutines の基礎と、Kotlin 1.9/2.0 における最新情報です。実装時は必ず公式リファレンスを参照し、バージョンアップに伴う挙動変更を確認してください。
参考リンク(すべて公式)
| 内容 | URL |
|---|---|
| Kotlin Coroutines 基本ガイド | https://kotlinlang.org/docs/coroutine-guide.html |
| AndroidX Lifecycle / viewModelScope | https://developer.android.com/topic/libraries/architecture/viewmodel#viewmodelscope |
| Retrofit + Coroutines 公式ドキュメント | https://developer.android.com/kotlin/coroutines#retrofit |
| kotlinx.coroutines.test(runTest) | https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/ |
| Turbine – Flow テストライブラリ | https://github.com/cashapp/turbine |
| Kotlin 1.9 Release Notes – Coroutines | https://kotlinlang.org/docs/whatsnew19.html#coroutine-improvements |
| Kotlin 2.0 Preview – Coroutines Improvements (JetBrains Blog) | https://blog.jetbrains.com/kotlin/2024/10/kotlin-2-preview-coroutine-improvements/ |
| WorkManager と Coroutines の公式ガイド | https://developer.android.com/topic/libraries/architecture/workmanager/coroutines |