Contents
Kotlin Coroutines エラー処理 ベストプラクティス|Android開発者のための非同期コード設計ガイド
Kotlin Coroutinesは2023年現在、1.7.2が最新安定版として活用され、Androidアプリケーションにおけるパフォーマンス・ユーザー体験向上に貢献する技術として注目されています。しかし、非同期処理においてエラーハンドリングを誤ると、アプリの信頼性やユーザーエクスペリエンスが著しく低下します。本記事では、Android開発におけるコルーチンエラーハンドリングのベストプラクティスを解説し、実際のデバッグケースも交えて具体的な設計パターンをお伝えします。
Structured Concurrencyによるエラーハンドリングの基本構造
Structured Concurrencyは、コルーチンのライフサイクルと密接に関連しています。このモデルでは、スコープ内での例外処理が階層的に設計される仕組みがあり、アプリケーション全体の信頼性向上に直結します。
コルーチンスコープとライフサイクル管理
Structured Concurrencyでは、viewModelScopeやlifecycleScopeといったスコープ内で処理を実行し、ライフサイクルに合わせて自動的にキャンセルされます。これにより、画面遷移時のリーク防止が可能になります。
- ViewModel内での使用例
kotlin
viewModelScope.launch {
try {
val data = repository.fetchData()
_uiState.value = DataSuccess(data)
} catch (e: Exception) {
_uiState.value = DataError(e.message ?: "Unknown error")
}
}
例外処理の階層設計原則
エラーハンドリングは「スコープ単位で捕獲・伝播」される仕組みです。以下に基本的な設計パターンを示します。
| スコープレベル | 説明 |
|---|---|
| コアロジック(Repository層) | 例外を具体的な型(NetworkException、DatabaseExceptionなど)でキャッチし、上位スコープへ適切に伝播 |
| ViewModel層 | 障害発生時のUI更新(エラーメッセージ表示など)を担当 |
| プレゼンテーション層 | ユーザー向けのフィードバック(SnackBar、ダイアログなど)を処理 |
注意点: エラー処理は「スコープ外でキャッチできない」という特性に注意してください。ライフサイクルに基づいた適切なスコープ選定が重要です。
launchとasyncにおける例外伝搬メカニズムの違い
非同期タスクの実行方法によって、エラーハンドリングの挙動が異なります。launchとasyncそれぞれの特徴を比較します。
Jobインターフェースでの異常処理
| 項目 | launch |
async |
|---|---|---|
| 例外伝搬 | スコープ内に保留される(外部でキャッチ不可) | 結果取得時に自動的にスローされる |
| 戻り値の有無 | 無し | Deferred<T>型として結果を取得可能 |
-
launchでの例
kotlin
launch {
try {
val result = fetchData()
} catch (e: Exception) {
// この例外は外側のtry/catchでは拾えない
}
} -
asyncでの例(エラーハンドリングを含む)
kotlin
val deferred = async {
try {
fetchData()
} catch (e: Exception) {
throw e // 明示的に例外をスロー
}
}
try {
val result = deferred.await()
} catch (e: Exception) {
// await()で例外がスローされる
Log.e("CoroutineError", "Async error occurred: ${e.message}")
}
ポイント:
launchはエラーを保留するため、外側のtry/catchでは拾えません。asyncは結果取得時に例外をスローする仕組みを利用する必要があります。
supervisorScopeの活用シーンと誤解ポイント
supervisorScopeは通常のコルーチンスコープとは異なり、子コルーチン間で例外が隔離される特徴を持っています。これは特定のシナリオに適しています。
子コルーチン間の例外隔離
supervisorScopeを使用することで、ある子コルーチンでのエラーが他の子コルーチンに影響を与えないようにできます。
-
ViewModel内での使用例(非同期通信とキャッシュ更新並行)
kotlin
supervisorScope {
launch {
try {
val data = networkService.fetchData()
_data.value = data
} catch (e: Exception) {
// キャッシュの読み込みを継続
Log.w("CoroutineError", "Network error occurred, but cache update continues")
}
}launch {
try {
cacheManager.updateCache()
} catch (e: IOException) {
Log.e("CoroutineError", "Cache update failed due to I/O exception")
}
}
}
適切なスコープ選定ガイドライン
| ケース | 推奨スコープ |
|---|---|
| フォーム入力処理(一部エラーを許容) | supervisorScope |
| 複数の非同期リクエストが並行実行される場面(失敗しても他は続行) | supervisorScope |
| 単一タスクで失敗すると全体キャンセルが必要なケース | 通常スコープ |
注意点: 過剰に
supervisorScopeを使用すると、エラーの監視が困難になる可能性があります。用途を明確にしてから選択しましょう。
コルーチンキャンセルとエラー処理の連携方法
コルーチンキャンセルは例外として扱われるため、エラーハンドリングとの連携が必要です。特にAndroidアプリでは、画面遷移やバックグラウンド処理におけるキャンセル処理が重要です。
キャンセル例外と通常例外の区別
- キャンセル例外(
CancellationException) - ユーザー操作により発生
isCancellationException()で判定可能- 通常例外
- 実行中の処理中に発生
- 一般的なtry/catchで処理
Jobインターフェースを介したステータス監視
Jobインターフェースを使うことで、コルーチンの状態(キャンセル中/正常終了)をリアルタイムで確認できます。
- ViewModel内での例
kotlin
private var job: Job? = null
fun fetchData() {
job?.cancel()
job = viewModelScope.launch {
try {
val data = repository.fetchData()
_uiState.value = DataSuccess(data)
} catch (e: Exception) {
if (!e.isCancellationException()) {
_uiState.value = DataError(e.message ?: "Unexpected error")
}
}
}
}
実際のAndroidアプリケーションでのデバッグケース
実務では、ライフサイクルとコルーチンの連携が不十分な場合、以下のエラーが発生します。
画面遷移時のコルーチンリーク回避
- 問題事象: ユーザーが画面を離脱した際にもコルーチンが実行され続けてしまう。
- 解決策:
viewModelScopeやlifecycleScopeを用いることで、ライフサイクルに合わせた自動キャンセルが可能。
バックグラウンド処理における例外監視
- 事例: 複数の非同期リクエストが並行して実行され、1つが失敗した際、他のリクエストも停止してしまう。
- 対応方法:
supervisorScopeを使用し、子コルーチン同士の影響を隔離。
実践的なエラーハンドリングコードサンプル
以下に具体的な例外処理ケースを示します。
1. ネットワークリクエスト時のエラーハンドリング
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
viewModelScope.launch { try { val data = repository.fetchData() _uiState.value = DataSuccess(data) } catch (e: IOException) { _uiState.value = DataError("ネットワーク接続に失敗しました") } catch (e: JSONException) { _uiState.value = DataError("データ形式が不正です") } catch (e: Exception) { _uiState.value = DataError("予期せぬエラーが発生しました") } } |
2. キャッシュ更新時のエラーハンドリング
|
1 2 3 4 5 6 7 8 9 |
viewModelScope.launch { try { val cachedData = cacheManager.getCachedData() _uiState.value = DataSuccess(cachedData) } catch (e: CacheException) { _uiState.value = DataError("キャッシュ読み込みに失敗しました") } } |
まとめ
- Structured Concurrencyはスコープとライフサイクルに基づく設計が不可欠で、特に
viewModelScopeやlifecycleScopeの利用が重要です。 launchでは外側での例外処理ができないため、asyncを結果取得時にエラーハンドリングする場面も存在します。supervisorScopeは子コルーチンのエラー隔離に適していますが、過剰な使用には注意が必要です。- キャンセル例外と通常例外の区別を明確にし、Jobインターフェースでステータス監視を行うことで、信頼性の高いアプリケーション設計が可能です。
- 実務ではライフサイクル管理と連動した設計が信頼性向上の鍵となります。
あなたのプロジェクトでも、コルーチンエラーハンドリングを改善してみてください!