Communitygithub.com

Kotlin Coroutines 與 Flow

適用於 Android 和 KMP 的 Kotlin Coroutines 與 Flow 模式 — 結構化並行、Flow 運算子、StateFlow、錯誤處理及測試。在 Android 或 KMP 上撰寫協程或 Flow 程式碼,或除錯取消與並行時使用。

Kotlin Coroutines 與 Flow 是什麼?

此技能提供一套實用模式,用於在 Android 與 Kotlin Multiplatform(KMP)專案中撰寫結構化並行、反應式資料流以及協程測試,讓開發者能夠編寫更安全、可維護的非同步程式碼。技能涵蓋結構化並行的正確層級層級(viewModelScope、coroutineScope、supervisorScope),並展示如何利用 `coroutineScope` 與 `async` 進行平行分解,同時透過 `supervisorScope` 處理子任務失敗而不影響其他任務。在 Flow 部分,詳細介紹了冷串流轉換、使用 `stateIn` 搭配 `WhileSubscribed` 管理 UI 狀態、多個 Flow 的合併、常用運算子(debounce、distinctUntilChanged、flatMapLatest、retryWhen)以及用 `SharedFlow` 處理一次性事件。另外也包含 Dispatchers 的選擇建議、協程取消的協作機制與清理,以及使用 Turbine 和 TestDispatcher 進行測試的實際範例,並列出常見的反模式。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/kotlin-coroutines-flows

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Kotlin Coroutines 與 Flow 是做什麼的?

適用於 Android 與 Kotlin Multiplatform 專案中結構化並行、基於 Flow 的反應式資料流,以及協程測試的模式。

何時啟用

  • 使用 Kotlin 協程撰寫非同步程式碼
  • 使用 Flow、StateFlow 或 SharedFlow 處理反應式資料
  • 處理並行操作(平行載入、防抖、重試)
  • 測試協程與 Flow
  • 管理協程作用域與取消

結構化並行

作用域層級

Application
  └── viewModelScope (ViewModel)
        └── coroutineScope { } (結構化子協程)
              ├── async { } (並行任務)
              └── async { } (並行任務)

始終使用結構化並行 — 絕不使用 GlobalScope:

// 錯誤
GlobalScope.launch { fetchData() }

// 良好 — 作用域在 ViewModel 生命週期內
viewModelScope.launch { fetchData() }

// 良好 — 作用域在 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 模式

冷 Flow — 一次性轉換為串流

fun observeItems(): Flow<List<Item>> = flow {
    // 每當資料庫變更時重新發射
    itemDao.observeAll()
        .map { entities -> entities.map { it.toDomain() } }
        .collect { emit(it) }
}

用 StateFlow 管理 UI 狀態

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("項目已刪除"))
        }
    }
}

// 在 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 或透過依賴注入提供。

取消

協作式取消

長時間執行的迴圈必須檢查取消狀態:

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 `搜尋更新項目列表`() = runTest {
    val fakeRepository = FakeItemRepository().apply { emit(testItems) }
    val viewModel = ItemListViewModel(GetItemsUseCase(fakeRepository))

    viewModel.state.test {
        assertEquals(ItemListState(), awaitItem())  // 初始狀態

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

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

使用 TestDispatcher 測試

@Test
fun `平行載入正確完成`() = 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

參考資料

請參閱技能:compose-multiplatform-patterns 以了解 Flow 的 UI 消費方式。 請參閱技能:android-clean-architecture 以了解協程在各層級中的定位。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相關技能