android-coroutines について
Android 開発者向けの Kotlin Coroutines スキルです。データ取得やバックグラウンド処理に役立つ、構造化並行処理、ライフサイクル安全性、エラー処理、テストの指針を示します。
主な機能
- 01CoroutineDispatcher の注入
- 02安全な Flow 収集
- 03viewModelScope の使用
- 04キャンセルと例外の処理
- 05callbackFlow によるコールバック変換
- 06runTest によるテスト
活用シーン
- 01API やデータベースからのデータ取得
- 02コールバック API の Flow 変換
- 03ライフサイクルに伴うタスク漏れの防止
- 04コルーチンと ViewModel のテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ new-silvermoon/ awesome-android-agent-skills --skill android-coroutinesインストール後、Claude Code で/android-coroutinesを入力して読み込まれたか確認します。
詳細
Android Coroutines エキスパートスキル
このスキルは、Android で本番品質の Kotlin Coroutines コードを記述するための、信頼できるルールとパターンを提供します。構造化された並行処理、ライフサイクルの安全性、最新のベストプラクティス(2025 年の標準)を徹底します。
責務
- 非同期ロジック:suspend 関数、Dispatcher の管理、並列実行の実装。
- リアクティブストリーム:
Flow、StateFlow、SharedFlow、callbackFlowの実装。 - ライフサイクルとの統合:スコープ(
viewModelScope、lifecycleScope)の管理と、安全な収集(repeatOnLifecycle)。 - エラー処理:
CoroutineExceptionHandler、SupervisorJob、適切なtry-catchの階層の実装。 - キャンセル可能性:
ensureActive()を使用して、長時間実行される処理を協調的にキャンセルできるようにすること。 - テスト:
TestDispatcherとrunTestの設定。
適用条件
ユーザーが次のような依頼をした場合、このスキルを有効にします。
- 「API/データベースからデータを取得して」
- 「バックグラウンド処理を実行して」
- スレッドやタスクに関連する「メモリリークを修正して」
- 「リスナー/コールバックを Coroutines に変換して」
- 「ViewModel を実装して」
- 「UI の状態更新を処理して」
重要なルールと制約
1. Dispatcher の注入(テスト容易性)
- クラス内で Dispatcher(例:
Dispatchers.IO、Dispatchers.Default)をハードコードしてはなりません。 - 必ずコンストラクター経由で
CoroutineDispatcherを注入してください。 - 利便性のため、コンストラクター引数ではデフォルトで
Dispatchers.IOを指定し、オーバーライドできるようにしてください。
// CORRECT
class UserRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) { ... }
// INCORRECT
class UserRepository {
fun getData() = withContext(Dispatchers.IO) { ... }
}
2. メインセーフティ
- Data レイヤーまたは Domain レイヤーで定義するすべての suspend 関数は、メインセーフでなければなりません。
- 単発の呼び出しは
suspend関数として公開してください。 - データの変更は
Flowとして公開してください。 - 呼び出し元(ViewModel)は、UI をブロックせずに
Dispatchers.Mainから呼び出せる必要があります。 - リポジトリーの実装内で
withContext(dispatcher)を使用して、実行をバックグラウンドに移してください。
3. ライフサイクルを考慮した収集
lifecycleScope.launchまたはlaunchWhenStarted(非推奨/安全ではありません)内でフローを直接収集してはなりません。- Activity または Fragment でフローを収集する場合は、必ず
repeatOnLifecycle(Lifecycle.State.STARTED)を使用してください。
// CORRECT
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { ... }
}
}
4. ViewModel スコープの使用
- ViewModel でコルーチンを開始する場合は
viewModelScopeを使用してください。 - ViewModel から View に suspend 関数を公開しないでください。ViewModel は、View が監視する
StateFlowまたはSharedFlowを公開してください。
5. 可変状態のカプセル化
MutableStateFlowまたはMutableSharedFlowを公開してはなりません。.asStateFlow()を使用するか、上位型にキャストして、読み取り専用のStateFlowまたはFlowとして公開してください。
6. GlobalScope の禁止
GlobalScopeは使用してはなりません。構造化された並行処理を損ない、リークにつながります。- 現在のスコープを超えてタスクを存続させる必要がある場合は、注入した
applicationScope(Application のライフサイクルに紐づくカスタムスコープ)を使用してください。
7. 例外処理
CancellationExceptionを再スローせずに、一般的なcatch (e: Exception)ブロックで捕捉してはなりません。runCatchingを使用する場合は、必ずCancellationExceptionを明示的に再スローしてください。CoroutineExceptionHandlerはトップレベルのコルーチン(launch内)にのみ使用してください。asyncや子コルーチン内では効果がありません。
8. キャンセル可能性
- コルーチンのキャンセルは協調的です。キャンセルを確認しない限り、直ちに停止することはありません。
- 大きなリストの処理やファイルの読み取りなど、負荷の高いループでは、キャンセルを確認するために必ず
ensureActive()またはyield()を呼び出してください。 delay()やwithContext()などの標準関数は、すでにキャンセル可能です。
9. コールバックの変換
- コールバックベースの API を Flow に変換するには
callbackFlowを使用してください。 callbackFlowブロックの末尾では必ずawaitCloseを使用して、リスナーの登録を解除してください。
コードパターン
Flow を使用した Repository パターン
class NewsRepository(
private val remoteDataSource: NewsRemoteDataSource,
private val externalScope: CoroutineScope, // For app-wide events
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val newsUpdates: Flow<List<News>> = flow {
val news = remoteDataSource.fetchLatestNews()
emit(news)
}.flowOn(ioDispatcher) // Upstream executes on IO
}
並列実行
suspend fun loadDashboardData() = coroutineScope {
val userDeferred = async { userRepo.getUser() }
val feedDeferred = async { feedRepo.getFeed() }
// Wait for both
DashboardData(
user = userDeferred.await(),
feed = feedDeferred.await()
)
}
runTest を使用したテスト
@Test
fun testViewModel() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val viewModel = MyViewModel(testDispatcher)
viewModel.loadData()
advanceUntilIdle() // Process coroutines
assertEquals(expectedState, viewModel.uiState.value)
}
コメント
Markdown 対応