Skip to content
cccskills
FreePublished on GitHub

kotlin-coroutines-flows

AndroidおよびKMPのためのKotlinコルーチンとFlowパターン — 構造化された並行処理、Flowオペレーター、StateFlow、エラーハンドリング、テスト。

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md8.4 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Kotlinコルーチン & Flow

AndroidおよびKotlin Multiplatformプロジェクトにおける構造化された並行処理、Flowベースのリアクティブストリーム、コルーチンテストのパターン。

アクティベーション条件

  • Kotlinコルーチンによる非同期コードの作成
  • Flow、StateFlow、SharedFlowによるリアクティブデータの使用
  • 並行処理の実装(並列ロード、デバウンス、リトライ)
  • コルーチンとFlowのテスト
  • コルーチンスコープとキャンセルの管理

構造化された並行処理

スコープ階層

Application
  └── viewModelScope (ViewModel)
        └── coroutineScope { } (構造化された子)
              ├── async { } (並行タスク)
              └── async { } (並行タスク)

常に構造化された並行処理を使用 — 決してGlobalScopeを使わない:

// BAD
GlobalScope.launch { fetchData() }

// GOOD — ViewModelのライフサイクルにスコープ
viewModelScope.launch { fetchData() }

// GOOD — Composableのライフサイクルにスコープ
LaunchedEffect(key) { fetchData() }

並列分解

並列処理にはcoroutineScope + asyncを使用:

suspend fun loadDashboard(): Dashboard = coroutineScope {
    val items = async { itemRepository.getRecent() }
    val stats = async { statsRepository.getToday() }
    val profile = async { userRepository.getCurrent() }
    Dashboard(
        items = items.await(),
        stats = stats.await(),
        profile = profile.await()
    )
}

SupervisorScope

子の失敗が兄弟をキャンセルしてはいけない場合にsupervisorScopeを使用:

suspend fun syncAll() = supervisorScope {
    launch { syncItems() }       // ここでの失敗はsyncStatsをキャンセルしない
    launch { syncStats() }
    launch { syncSettings() }
}

Flowパターン

Cold Flow — ワンショットからストリームへの変換

fun observeItems(): Flow<List<Item>> = flow {
    // データベースが変更されるたびに再発行
    itemDao.observeAll()
        .map { entities -> entities.map { it.toDomain() } }
        .collect { emit(it) }
}

UI状態のためのStateFlow

class DashboardViewModel(
    observeProgress: ObserveUserProgressUseCase
) : ViewModel() {
    val progress: StateFlow<UserProgress> = observeProgress()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = UserProgress.EMPTY
        )
}

WhileSubscribed(5_000)は最後のサブスクライバーが離れた後5秒間アップストリームをアクティブに保つ — コンフィグレーション変更時に再起動なしで生き残る。

複数のFlowの結合

val uiState: StateFlow<HomeState> = combine(
    itemRepository.observeItems(),
    settingsRepository.observeTheme(),
    userRepository.observeProfile()
) { items, theme, profile ->
    HomeState(items = items, theme = theme, profile = profile)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), HomeState())

Flowオペレーター

// 検索入力のデバウンス
searchQuery
    .debounce(300)
    .distinctUntilChanged()
    .flatMapLatest { query -> repository.search(query) }
    .catch { emit(emptyList()) }
    .collect { results -> _state.update { it.copy(results = results) } }

// 指数バックオフ付きリトライ
fun fetchWithRetry(): Flow<Data> = flow { emit(api.fetch()) }
    .retryWhen { cause, attempt ->
        if (cause is IOException && attempt < 3) {
            delay(1000L * (1 shl attempt.toInt()))
            true
        } else {
            false
        }
    }

ワンタイムイベント用SharedFlow

class ItemListViewModel : ViewModel() {
    private val _effects = MutableSharedFlow<Effect>()
    val effects: SharedFlow<Effect> = _effects.asSharedFlow()

    sealed interface Effect {
        data class ShowSnackbar(val message: String) : Effect
        data class NavigateTo(val route: String) : Effect
    }

    private fun deleteItem(id: String) {
        viewModelScope.launch {
            repository.delete(id)
            _effects.emit(Effect.ShowSnackbar("Item deleted"))
        }
    }
}

// Composableで収集
LaunchedEffect(Unit) {
    viewModel.effects.collect { effect ->
        when (effect) {
            is Effect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message)
            is Effect.NavigateTo -> navController.navigate(effect.route)
        }
    }
}

ディスパッチャー

// CPU集約的な処理
withContext(Dispatchers.Default) { parseJson(largePayload) }

// IO バウンドな処理
withContext(Dispatchers.IO) { database.query() }

// メインスレッド(UI) — viewModelScopeのデフォルト
withContext(Dispatchers.Main) { updateUi() }

KMPではDispatchers.DefaultとDispatchers.Mainを使用(全プラットフォームで利用可能)。Dispatchers.IOはJVM/Androidのみ — 他のプラットフォームではDispatchers.Defaultを使用するか、DIで提供。

キャンセル

協調的キャンセル

長時間ループはキャンセルをチェックする必要がある:

suspend fun processItems(items: List<Item>) = coroutineScope {
    for (item in items) {
        ensureActive()  // キャンセルされた場合CancellationExceptionをスロー
        process(item)
    }
}

try/finallyによるクリーンアップ

viewModelScope.launch {
    try {
        _state.update { it.copy(isLoading = true) }
        val data = repository.fetch()
        _state.update { it.copy(data = data) }
    } finally {
        _state.update { it.copy(isLoading = false) }  // キャンセル時も常に実行
    }
}

テスト

TurbineによるStateFlowテスト

@Test
fun `search updates item list`() = runTest {
    val fakeRepository = FakeItemRepository().apply { emit(testItems) }
    val viewModel = ItemListViewModel(GetItemsUseCase(fakeRepository))

    viewModel.state.test {
        assertEquals(ItemListState(), awaitItem())  // 初期値

        viewModel.onSearch("query")
        val loading = awaitItem()
        assertTrue(loading.isLoading)

        val loaded = awaitItem()
        assertFalse(loaded.isLoading)
        assertEquals(1, loaded.items.size)
    }
}

TestDispatcherによるテスト

@Test
fun `parallel load completes correctly`() = runTest {
    val viewModel = DashboardViewModel(
        itemRepo = FakeItemRepo(),
        statsRepo = FakeStatsRepo()
    )

    viewModel.load()
    advanceUntilIdle()

    val state = viewModel.state.value
    assertNotNull(state.items)
    assertNotNull(state.stats)
}

Flowのフェイク化

class FakeItemRepository : ItemRepository {
    private val _items = MutableStateFlow<List<Item>>(emptyList())

    override fun observeItems(): Flow<List<Item>> = _items

    fun emit(items: List<Item>) { _items.value = items }

    override suspend fun getItemsByCategory(category: String): Result<List<Item>> {
        return Result.success(_items.value.filter { it.category == category })
    }
}

避けるべきアンチパターン

  • GlobalScopeの使用 — コルーチンがリークし、構造化されたキャンセルがない
  • スコープなしのinit {}でのFlow収集 — viewModelScope.launchを使用
  • ミュータブルコレクションでのMutableStateFlow使用 — 常にイミュータブルコピーを使用:_state.update { it.copy(list = it.list + newItem) }
  • CancellationExceptionのキャッチ — 適切なキャンセルのために伝播させる
  • flowOn(Dispatchers.Main)での収集 — 収集ディスパッチャーは呼び出し元のディスパッチャー
  • @Composable内でrememberなしのFlow作成 — 再コンポジションのたびにFlowが再作成される

参考

Flowの UI での利用については、スキル compose-multiplatform-patterns を参照。 コルーチンがレイヤーのどこに位置するかは、スキル android-clean-architecture を参照。

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

コーディングエージェント(Claude Code、Aider、Codexなど)のカスタムタスクによる直接比較。合格率、コスト、時間、一貫性メトリクスを測定

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AIエージェントのアクションスペース、ツール定義、オブザベーションフォーマットを設計・最適化し、タスク完了率を向上させる

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

評価ファーストの実行、分解、コスト考慮型モデルルーティングによるエージェンティックエンジニアとしての運用

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AIエージェントが実装出力の大部分を生成するチームのためのエンジニアリング運用モデル

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AI支援開発のためのリグレッションテスト戦略。データベース依存なしのサンドボックスモードAPIテスト、自動バグチェックワークフロー、同一モデルがコードの作成とレビューを行う際のAIの盲点を検出するパターン。

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AndroidおよびKotlin Multiplatformプロジェクトのためのクリーンアーキテクチャパターン — モジュール構成、依存関係ルール、UseCase、Repository、データレイヤーパターン

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

See all skills by hage-oyaji

Report a problem with this skill