Communitygithub.com

Kotlin Coroutines 与 Flow

适用于 Android 和 KMP 的 Kotlin 协程与 Flow 模式——结构化并发、Flow 操作符、StateFlow、错误处理和测试。在编写 Android 或 KMP 的协程或 Flow 代码,或调试取消和并发问题时使用。

Kotlin Coroutines 与 Flow 是什么?

此技能为 Android 和 Kotlin Multiplatform (KMP) 项目中的 Kotlin 协程与 Flow 开发提供了一套全面的模式。它涵盖了结构化并发(包括作用域层次结构、并行分解和使用 SupervisorScope 的错误处理)、用于反应式数据流的 Flow 模式(StateFlow 用于 UI 状态、SharedFlow 用于一次性事件、以及 combine、debounce、flatMapLatest 等关键操作符)以及用于正确进行 CPU 密集型、IO 绑定或 UI 工作的调度器选择。此外,该技能还详细介绍了协作式取消、使用 try/finally 进行清理,以及使用 Turbine 测试 StateFlow、使用 TestDispatcher 和创建用于模拟依赖关系的假 Flow 的测试策略。它基于 SKILL.md 文档构建,旨在作为一种参考,帮助开发者避免常见的反模式(如全局作用域或可变集合),并编写出健壮、可维护的协程代码。

兼容平台~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 与 Flows

适用于 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) }
}

用于 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 `搜索更新项目列表`() = 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 `并行加载正确完成`() = 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

参考资料

有关 UI 消费 Flow,请参阅技能:compose-multiplatform-patterns 有关协程在层级中的位置,请参阅技能: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.

相关技能