mcpField
K
kotlin-coroutines

kotlin-coroutines

github.com/rcosteira79开发165162 次安装6 周前更新
代码仓库

关于 kotlin-coroutines

面向编码 Agent 的 Kotlin Coroutines 参考技能,聚焦结构化并发、作用域所有权、取消与异常处理,也涵盖 Kotlin Multiplatform 和协程测试建议。

主要功能

  1. 01结构化并发与作用域所有权
  2. 02兼顾取消的异常处理
  3. 03Dispatcher 与主线程安全指导
  4. 04Kotlin Multiplatform 作用域实践
  5. 05协程测试提示

适用场景

  1. 01审查协程作用域的所有权
  2. 02安全处理取消与异常
  3. 03设计 KMP 协程 API
  4. 04规划协程和 Dispatcher 测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rcosteira79/android-skills --skill kotlin-coroutines

安装后在 Claude Code 中输入/kotlin-coroutines确认已加载。

详细介绍

Kotlin 协程

基于结构化并发:每个协程都在一个作用域中运行;取消和错误沿着父子层级传播。本参考重点介绍最常出错的两项原则:谁拥有作用域,以及如何处理异常而不破坏取消机制。

**主线程安全:**执行阻塞工作的函数负责使用 withContext(ioDispatcher);调用方调用挂起函数前不要切换调度器。

作用域所有权 — 优先使用 suspend fun,让调用方拥有作用域

在非 UI 类(存储库、管理器、用例、数据源)中保存 CoroutineScope,是一个强烈的代码审查警示信号:该类必须证明自己负责取消、错误报告、重启和生命周期——但大多数类做不到。解决方法几乎总是:将 API 改为 suspend,让调用方拥有作用域。

// DO — suspend fun; the caller owns the scope, cancellation propagates, exceptions surface
class ArticlesRepository(private val dataSource: ArticlesDataSource, private val io: CoroutineDispatcher) {
    suspend fun bookmark(article: Article) = withContext(io) { dataSource.bookmark(article) }
}
class BookmarkViewModel(private val repo: ArticlesRepository) : ViewModel() {
    fun onBookmark(a: Article) { viewModelScope.launch { repo.bookmark(a) } }
}

// DO NOT — store a scope and launch inside the repository
class ArticlesRepository(private val externalScope: CoroutineScope, /* ... */) {
    suspend fun bookmark(a: Article) { externalScope.launch { dataSource.bookmark(a) }.join() }
}

**保存作用域为何危险:**作用域一旦被取消,此后在该作用域上执行的每个 launch 都会静默地以取消状态完成——没有异常,也没有日志。如果取消是由进程终止或配置错误的 DI 图引起的,该类仍会接受调用,却什么也不做,且不会发出任何提示。

工作必须在调用方结束后继续时

用户离开页面后仍必须完成的写入操作,不属于存储库——它应交由 WorkManager 任务处理,或交给一个具名的应用级类,该类有意拥有自己的作用域(例如注入 applicationScope 的 OfflineBookmarkQueue,仅在进程终止时取消)。具名类能明确、可测试且可观测地表明其生命周期——不像隐藏在某处的 externalScope.launch。

状态持有者例外 — 非挂起方法中何时可以使用 launch

UI 状态持有者只有在同时满足以下三项时,才能从非挂起事件回调中启动协程:(1)它确实拥有由视图层收集的 UI 状态;(2)它使用绑定到生命周期的作用域(viewModelScope / rememberCoroutineScope);(3)触发源是 UI 事件(点击、滑动、生命周期)——而不是存储库调用、后台计时器或 DI 钩子。任何一项不满足,都应公开 suspend fun,让真正的状态持有者拥有作用域。

反模式:init { launch } 和 DI 单例中的 launch

init { viewModelScope.launch { while (isActive) { … } } 会让工作变得不可见——没有具名触发器、没有重启路径,而且导航返回或前进时会悄悄重新启动。应改为通过 stateIn 暴露 StateFlow。同样,从构造函数中启动协程的 @Singleton(或 Hilt Initializer)会让协程“在 DI 实例化我的任何时候”启动,无人能观察到它——应使用 WorkManager.enqueueUniquePeriodicWork、将工作反转到消费者中,或将其放入具有显式 startX() 方法的具名类中。

诊断任何由 DI 管理或使用已保存作用域的 launch:启动时机在哪里定义?谁能观察它是否正在运行?不重启进程就能重新启动它吗? 如果答案是*“无处不在 / 无人 / 不能”*,就意味着需要重构。

取消

协作式取消:在长循环开头调用 ensureActive()(取消时会抛出异常),使用 isActive 检查但不抛出异常,使用 yield() 还能让出线程。delay / withContext 本身就支持取消。取消后必须执行的清理工作应放在 finally { withContext(NonCancellable) { … } } 中——NonCancellable 只能用于此处,不能作为通用的逃生通道。withTimeout 抛出的 TimeoutCancellationException 也是 CancellationException——捕获它时绝不能不重新抛出。(网络超时不要使用 withTimeout——这些由 OkHttp/Retrofit/Ktor 管理。)

异常处理——不要破坏取消机制

// DO — specific types
try { repo.login(u, p) } catch (e: IOException) { _state.value = Error("Network") }

// DO NOT — catch (e: Exception) / Throwable swallows CancellationException and breaks structured cancellation
  • 捕获特定类型(IOException、HttpException),不要捕获 Exception / Throwable。
  • 绝不要捕获 CancellationException 后不重新抛出。
  • 绝不要在挂起函数中使用 runCatching——它会捕获 CancellationException。请使用会重新抛出该异常的 suspendRunCatching:
suspend inline fun <R> suspendRunCatching(block: () -> R): Result<R> = try {
    Result.success(block())
} catch (e: CancellationException) { throw e } catch (e: Throwable) { Result.failure(e) }
  • try/catch 不会包裹 launch {}——请将其放在协程体内部。
  • 在 supervisorScope 中未等待的 async {} 会吞掉其异常——如果不需要结果,请使用 launch。
  • viewModelScope 已经使用了 SupervisorJob——不要再额外添加一个。

KMP

不要使用 MainScope()——它是非结构化的(Dispatchers.Main + SupervisorJob()),存在与 GlobalScope 相同的问题。请从平台层注入绑定生命周期的 CoroutineScope(Android 上使用 viewModelScope;iOS 上使用绑定到视图控制器的作用域)。为平台实现注入 CoroutineDispatcher;如果某个目标平台缺少 Dispatchers.Main.immediate,请使用 expect/actual。

测试

协程和调度器测试见 android-skills:android-testing——尤其要注意双调度器陷阱(runTest 和 MainDispatcherRule 必须共用一个调度器),以及 Compose 测试中 StandardTestDispatcher 的默认设置。

评论

支持 Markdown