kotlin-concurrency-expert について
kotlin-concurrency-expert は、Android プロジェクトの Kotlin Coroutines の問題をレビューし、修正するスキルです。構造化並行性とライフサイクル安全性を重視し、動作の変更を抑えます。
主な機能
- 01クラッシュ、リーク、ANR、競合の診断
- 02ライフサイクルに結び付いたスコープ選択
- 03ブロッキング処理を適切な Dispatcher へ移動
- 04キャンセル処理と Flow 状態のカプセル化
- 05後処理付き callbackFlow への変換
- 06テスト用 Dispatcher の注入
活用シーン
- 01Android のコルーチン関連 ANR の調査
- 02GlobalScope を置き換えてライフサイクルリークを防止
- 03UI ライフサイクルに沿った Flow の収集
- 04コルーチンのキャンセル処理とテスト容易性の改善
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ new-silvermoon/ awesome-android-agent-skills --skill kotlin-concurrency-expertインストール後、Claude Code で/kotlin-concurrency-expertを入力して読み込まれたか確認します。
詳細
Kotlin Concurrency Expert
概要
構造化並行性、ライフサイクルの安全性、適切なスコープ設定、最新のベストプラクティスを適用し、動作への変更を最小限に抑えながら、Android のコードベースにおける Kotlin Coroutines の問題をレビューして修正します。
ワークフロー
1. 問題をトリアージする
- 正確なエラー、クラッシュ、または症状(ANR、メモリリーク、競合状態、不正な状態)を把握します。
- プロジェクトの Coroutines の設定を確認します:
kotlinx-coroutines-androidのバージョン、lifecycle-runtime-ktxのバージョン。 - 現在のスコープコンテキスト(
viewModelScope、lifecycleScope、カスタムスコープ、またはスコープなし)を特定します。 - コードが UI に関連するもの(
Dispatchers.Main)なのか、メインスレッド以外での実行を意図したもの(Dispatchers.IO、Dispatchers.Default)なのかを確認します。 - テスト容易性のための Dispatcher のインジェクションパターンを検証します。
2. 最小限で安全な修正を適用する
構造化並行性とライフサイクルの安全性を確保しつつ、既存の動作を維持できる編集を優先します。
よくある修正:
- ANR/メインスレッドのブロック:重い処理を
withContext(Dispatchers.IO)またはDispatchers.Defaultに移動し、サスペンド関数がメインセーフであることを確認します。 - メモリリーク/ゾンビコルーチン:
GlobalScopeをライフサイクルに紐付いたスコープ(viewModelScope、lifecycleScope、またはインジェクションされたapplicationScope)に置き換えます。 - ライフサイクルに応じた収集の問題:非推奨の
launchWhenStartedをrepeatOnLifecycle(Lifecycle.State.STARTED)に置き換えます。 - 状態の公開:
MutableStateFlow/MutableSharedFlowをカプセル化し、読み取り専用のStateFlowまたはFlowを公開します。 - CancellationException の握りつぶし:汎用的な
catch (e: Exception)ブロックでCancellationExceptionを再スローするようにします。 - 協調しないキャンセル:協調キャンセルを可能にするため、タイトなループに
ensureActive()またはyield()を追加します。 - コールバック API:リスナーを
callbackFlowに変換し、適切なawaitCloseによるクリーンアップを行います。 - Dispatcher のハードコード:テスト容易性のため、コンストラクター経由で
CoroutineDispatcherをインジェクションします。
重要なルール
Dispatcher のインジェクション(テスト容易性)
// CORRECT: Inject dispatcher
class UserRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun fetchUser() = withContext(ioDispatcher) { ... }
}
// INCORRECT: Hardcoded dispatcher
class UserRepository {
suspend fun fetchUser() = withContext(Dispatchers.IO) { ... }
}
ライフサイクルを考慮した収集
// CORRECT: Use repeatOnLifecycle
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state -> updateUI(state) }
}
}
// INCORRECT: Direct collection (unsafe, deprecated)
lifecycleScope.launchWhenStarted {
viewModel.uiState.collect { state -> updateUI(state) }
}
状態のカプセル化
// CORRECT: Expose read-only StateFlow
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
// INCORRECT: Exposed mutable state
class MyViewModel : ViewModel() {
val uiState = MutableStateFlow(UiState()) // Leaks mutability
}
例外処理
// CORRECT: Rethrow CancellationException
try {
doSuspendWork()
} catch (e: CancellationException) {
throw e // Must rethrow!
} catch (e: Exception) {
handleError(e)
}
// INCORRECT: Swallows cancellation
try {
doSuspendWork()
} catch (e: Exception) {
handleError(e) // CancellationException swallowed!
}
協調キャンセル
// CORRECT: Check for cancellation in tight loops
suspend fun processLargeList(items: List<Item>) {
items.forEach { item ->
ensureActive() // Check cancellation
processItem(item)
}
}
// INCORRECT: Non-cooperative (ignores cancellation)
suspend fun processLargeList(items: List<Item>) {
items.forEach { item ->
processItem(item) // Never checks cancellation
}
}
コールバックの変換
// CORRECT: callbackFlow with awaitClose
fun locationUpdates(): Flow<Location> = callbackFlow {
val listener = LocationListener { location ->
trySend(location)
}
locationManager.requestLocationUpdates(listener)
awaitClose { locationManager.removeUpdates(listener) }
}
スコープのガイドライン
| スコープ | 使用する場面 | ライフサイクル |
|---|---|---|
viewModelScope | ViewModel の操作 | ViewModel とともにクリアされます |
lifecycleScope | Activity/Fragment での UI 操作 | ライフサイクルオーナーの破棄時に破棄されます |
repeatOnLifecycle | UI での Flow の収集 | ライフサイクルの状態に応じて開始/停止します |
applicationScope(インジェクション) | アプリ全体のバックグラウンド処理 | アプリケーションの存続期間 |
GlobalScope | 絶対に使用しない | 構造化並行性を損ないます |
テストパターン
@Test
fun `loading data updates state`() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val repository = FakeRepository()
val viewModel = MyViewModel(repository, testDispatcher)
viewModel.loadData()
advanceUntilIdle()
assertEquals(UiState.Success(data), viewModel.uiState.value)
}
参考資料
コメント
Markdown 対応