
ECC Kotlin 开发模式指南从空安全到协程的惯用法实战手册【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC导读本篇文章基于 ECC 仓库中的kotlin-patterns技能文档位于 .kiro/skills/kotlin-patterns/SKILL.md同时以同一名称托管于 skills/kotlin-patterns/SKILL.md系统讲解构建健壮、高效、可维护 Kotlin 应用的惯用模式涵盖空安全类型系统、不可变性与数据类、密封类穷尽分支、协程结构化并发、扩展函数、类型安全 DSL、委托与惰性序列以及 Gradle Kotlin DSL 工程配置。文中所有概念均结合仓库内的工程级约束文件如 rules/kotlin/patterns.md、skills/kotlin-coroutines-flows/SKILL.md与代码评审规范交叉印证。读完你将获得一套可以直接照抄的可运行代码模板并理解每条惯用法背后的取舍与反模式。一、这套模式的定位何时使用、如何运作kotlin-patterns技能文档把“惯用 Kotlin”收敛为七大关键领域的工程约束利用类型系统与安全调用运算符实现的空安全以val与数据类copy()为核心的默认不可变面向穷尽类型层级建模的密封类与密封接口基于协程与Flow的结构化并发以扩展函数实现的无继承能力扩展使用DslMarker与 lambda 接收者构造的类型安全 DSL面向构建配置的Gradle Kotlin DSL。该技能的使用场景被界定为五类编写新 Kotlin 代码、评审已有代码、重构存量代码、设计 Kotlin 模块/库以及配置 Gradle Kotlin DSL 构建。这与仓库中kotlin-reviewer评审 Agent见 agents/kotlin-reviewer.md在架构评审时的关注点高度一致——该 Agent 将!!滥用、var可用却用val、非穷尽when、对外暴露可变集合等列为需要上报的“Kotlin Idioms (MEDIUM)”检查项恰好是本文讨论反模式的机器可执行版本。二、空安全让类型系统替你消灭 NPEKotlin 类型系统在编译期区分可空与不可空类型这是与 Java 最大的生产力差异之一。正确姿势是默认使用不可空类型仅在语义上确实可能缺失时才声明可空并用安全调用?.与 Elvis?:收口。// Good: 使用不可空类型作为默认 fun getUser(id: String): User { return userRepository.findById(id) ?: throw UserNotFoundException(User $id not found) } // Good: 安全调用与 Elvis 运算符 fun getUserEmail(userId: String): String { val user userRepository.findById(userId) return user?.email ?: unknownexample.com } // Bad: 强解包可空类型 fun getUserEmail(userId: String): String { val user userRepository.findById(userId) return user!!.email // Throws NPE if null }几条实践要点?:的右侧不一定是字面量可以是表达式或抛出的异常用于把“数据缺失”收敛为“明确的失败语义”与 Java 互操作时平台类型platform type的可空性不可信。仓库评审规范在 rules/kotlin/patterns.md 与反模式清单中均强调 Java 传值必须显式判空kotlin-reviewerAgent 也会对任何非空断言给出提示推荐改用?.、?:、requireNotNull或checkNotNull空安全最易被破坏的场景是“构造期校验缺失”此时应配合require/check前置条件函数见后文错误处理章节。三、不可变优先val、copy()与不可变集合在 Kotlin 中“能val就val”数据结构优先使用不可变集合状态更新通过数据类copy()产生新实例而非原地修改。// Good: 不可变数据 data class User( val id: String, val name: String, val email: String, ) // Good: 用 copy() 做转换 fun updateEmail(user: User, newEmail: String): User user.copy(email newEmail) // Good: 不可变集合 val users: ListUser listOf(user1, user2) val filtered users.filter { it.email.isNotBlank() } // Bad: 可变全局状态 var currentUser: User? null // 尽量避免 val mutableUsers mutableListOfUser() // 仅在确有需要时使用不可变约定不仅是代码风格问题而是并发安全的基础设施当它与StateFlow组合时典型 UI 状态管理对状态内的可变集合做原地add不会触发重组/订阅方感知。因此rules/kotlin/patterns.md的 Repository 层约定“suspend函数返回ResultT或自定义错误类型、用Flow暴露响应式流”而 skills/kotlin-coroutines-flows/SKILL.md 的反模式清单明确写着MutableStateFlow中存放可变集合时必须用不可变拷贝更新即_state.update { it.copy(list it.list newItem) }。这也与 agents/kotlin-reviewer.md 中的 HIGH 级检查项“StateFlow holding mutable list”相互印证。四、表达式体与单表达式函数简洁而不失可读能用表达式体就不要写块体when作为表达式时天然穷尽且返回值// Good: 表达式体 fun isAdult(age: Int): Boolean age 18 fun formatFullName(first: String, last: String): String $first $last.trim() fun User.displayName(): String name.ifBlank { email.substringBefore() } // Good: when 作为表达式 fun statusMessage(code: Int): String when (code) { 200 - OK 404 - Not Found 500 - Internal Server Error else - Unknown status: $code } // Bad: 不必要的块体 fun isAdult(age: Int): Boolean { return age 18 }注意上例把扩展函数User.displayName()与表达式体结合使用是第四、第六两个主题的自然交汇——详见后文扩展函数章节。五、数据类与值类值对象的两种建模层次数据类自动获得equals/hashCode/toString/componentN/copy适合承载“以数据为主”的载体类型而JvmInline值类则在不引入运行时装箱开销的前提下提供类型安全包装。// Good: 数据类——自带 copy、equals、hashCode、toString data class CreateUserRequest( val name: String, val email: String, val role: Role Role.USER, ) // Good: 值类——零运行时开销的类型安全包装 JvmInline value class UserId(val value: String) { init { require(value.isNotBlank()) { UserId cannot be blank } } } JvmInline value class Email(val value: String) { init { require( in value) { Invalid email: $value } } } fun getUser(id: UserId): User userRepository.findById(id)两个建模技巧值得专门说明值类 init约束把合法性校验推进构造函数让非法状态“构造不出来”即所谓的“让非法状态不可表示”。Email构造时即验证存在后续所有使用方无需重复防御值类的init校验配合第 11 节的require使用是前置条件风格最内聚的应用点。六、密封类与密封接口用穷尽分支管理受限类型层级6.1 受限层级建模Result 类型对“成功 / 失败 / 加载中”这类受限状态集合密封类是首选因为它让when具备穷尽性exhaustive——编译器在新增子类型后强制所有分支同步更新。sealed class Resultout T { data class SuccessT(val data: T) : ResultT() data class Failure(val error: AppError) : ResultNothing() data object Loading : ResultNothing() } fun T ResultT.getOrNull(): T? when (this) { is Result.Success - data is Result.Failure - null is Result.Loading - null } fun T ResultT.getOrThrow(): T when (this) { is Result.Success - data is Result.Failure - throw error.toException() is Result.Loading - throw IllegalStateException(Still loading) }几点语义细节泛型参数声明为out T协变配合ResultNothing让无数据变体可以安放在任何具体类型的层级下data object LoadingKotlin 1.9 的data object保证Loading单例语义把when的每个分支都做成带名函数getOrNull/getOrThrow避免在业务代码里到处展开原始when。6.2 密封接口建模 API 错误错误即类型仓库的 error-handling 惯例倾向“用类型表达错误而不是用异常表达控制流”。密封接口可以把一整族 API 错误压缩为一个可穷尽的类型系统sealed interface ApiError { val message: String data class NotFound(override val message: String) : ApiError data class Unauthorized(override val message: String) : ApiError data class Validation( override val message: String, val field: String, ) : ApiError data class Internal( override val message: String, val cause: Throwable? null, ) : ApiError } fun ApiError.toStatusCode(): Int when (this) { is ApiError.NotFound - 404 is ApiError.Unauthorized - 401 is ApiError.Validation - 422 is ApiError.Internal - 500 }模式亮点通过toStatusCode()扩展函数把错误类型映射为 HTTP 状态码映射逻辑内聚、无分支遗漏——这正是kotlin-reviewerAgent 检查“when对密封类型是否穷尽”想要在代码库里强制出的形态。七、作用域函数五种工具各司其职let/apply/also/run/with是 Kotlin 出镜率最高的五个函数选择依据是返回什么与以何种方式接收对象// let: 转换可空值或作用域内结果 val length: Int? name?.let { it.trim().length } // apply: 配置对象返回对象本身 val user User().apply { name Alice email aliceexample.com } // also: 执行副作用返回对象本身 val user createUser(request).also { logger.info(Created user: ${it.id}) } // run: 以接收者执行代码块返回块的结果 val result connection.run { prepareStatement(sql) executeQuery() } // with: run 的非扩展形式 val csv with(StringBuilder()) { appendLine(name,email) users.forEach { appendLine(${it.name},${it.email}) } toString() }记忆口诀可以概括为想改对象配置用apply想记日志/埋点用also想在可空对象上取计算结果用let想复用接收者上下文并返回结果用runwith是脱离可空接收者的run。反模式嵌套作用域函数会严重伤害可读性。// Bad: 嵌套作用域函数 user?.let { u - u.address?.let { addr - addr.city?.let { city - println(city) // 难以阅读 } } } // Good: 直接链式安全调用 val city user?.address?.city city?.let { println(it) }八、扩展函数不靠继承增加行为扩展函数让“外部给类型加方法”成为可能适合领域化工具、集合语义补充以及把类型相关逻辑收拢在类内部用私有扩展避免污染全局命名空间// Good: 领域化扩展 fun String.toSlug(): String lowercase() .replace(Regex([^a-z0-9\\s-]), ) .replace(Regex(\\s), -) .trim(-) fun Instant.toLocalDate(zone: ZoneId ZoneId.systemDefault()): LocalDate atZone(zone).toLocalDate() // Good: 集合扩展 fun T ListT.second(): T this[1] fun T ListT.secondOrNull(): T? getOrNull(1) // Good: 局部作用域的扩展不污染全局命名空间 class UserService { private fun User.isActive(): Boolean status Status.ACTIVE lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) fun getActiveUsers(): ListUser userRepository.findAll().filter { it.isActive() } }工程上推荐把扩展函数声明在“离使用者最近”的作用域全局复用的放在顶层文件只服务于某个类内部的放在该类的私有成员区。仓库里也能看到同样的组织风格——例如 rules/kotlin/patterns.md 用operator fun invoke把 UseCase 调用点压缩为一次直接调用见第 12 节示例而 agents/kotlin-reviewer.md 则明确反对“Java 风格静态工具类”主张改用顶层函数。九、协程与 Flow结构化并发与冷流9.1coroutineScopevssupervisorScope结构化并发的核心承诺是子协程的生命周期被父作用域约束取消会沿结构传播。// Good: coroutineScope——任一子任务失败会取消整个作用域 suspend fun fetchUserWithPosts(userId: String): UserProfile coroutineScope { val userDeferred async { userService.getUser(userId) } val postsDeferred async { postService.getUserPosts(userId) } UserProfile( user userDeferred.await(), posts postsDeferred.await(), ) } // Good: supervisorScope——子任务可独立失败 suspend fun fetchDashboard(userId: String): Dashboard supervisorScope { val user async { userService.getUser(userId) } val notifications async { notificationService.getRecent(userId) } val recommendations async { recommendationService.getFor(userId) } Dashboard( user user.await(), notifications try { notifications.await() } catch (e: CancellationException) { throw e } catch (e: Exception) { emptyList() }, recommendations try { recommendations.await() } catch (e: CancellationException) { throw e } catch (e: Exception) { emptyList() }, ) }Dashboard 示例是仓库规约里反复出现的标准手法可对照 skills/kotlin-coroutines-flows/SKILL.md 中loadDashboard的并行分解与supervisorScope示例对可降级的子任务单独try/catch且必须先重抛CancellationException——吞掉取消异常会破坏结构化取消语义。这是kotlin-reviewerAgent 的 HIGH 级检查项原文给出的对照是// BAD —— 吞掉了取消 try { fetchData() } catch (e: Exception) { log(e) } // GOOD —— 保留取消传播 try { fetchData() } catch (e: CancellationException) { throw e } catch (e: Exception) { log(e) }9.2Flow面向响应式数据流的冷流flow { }构造的是冷流——每次收集都会重新执行生产者代码这使其天然适合“订阅数据库变化”“轮询”等场景。错误处理应在流内用.catch收敛fun observeUsers(): FlowListUser flow { while (currentCoroutineContext().isActive) { val users userRepository.findAll() emit(users) delay(5.seconds) } }.catch { e - logger.error(Error observing users, e) emit(emptyList()) } fun searchUsers(query: FlowString): FlowListUser query .debounce(300.milliseconds) .distinctUntilChanged() .filter { it.length 2 } .mapLatest { q - userRepository.search(q) } .catch { emit(emptyList()) }搜索场景的管道debounce → distinctUntilChanged → filter → mapLatest是 UI 层输入流的标准打法其中mapLatest保证只有最新一次查询结果会被下游接收。从仓库实现证据看这一主题被独立沉淀为专门技能 skills/kotlin-coroutines-flows/SKILL.md其中补充了三个本技能文档未展开、但工程上几乎必然遇到的知识点StateFlow化冷流stateIn(scope, SharingStarted.WhileSubscribed(5_000), initialValue)最后一个订阅者离开后仍保持上游活跃 5 秒可扛过配置变更而不重启数据源一次性事件用MutableSharedFlow如 Snackbar、导航这类“只应消费一次”的事件不应放入会保留最新值的StateFlow调度器选择CPU 密集用Dispatchers.DefaultIO 密集用Dispatchers.IO该调度器在 JVM/Android 可用KMP 其他平台需用Default或经 DI 提供。9.3 取消与清理长时间循环体必须在每个昂贵步骤前检查取消状态资源获取后即便被取消也必须释放且释放动作要放进NonCancellable上下文否则finally里的挂起调用在取消时同样会被中断// Good: 尊重取消 suspend fun processItems(items: ListItem) { items.forEach { item - ensureActive() // 在昂贵工作前检查取消 processItem(item) } } // Good: 用 try/finally 清理 suspend fun acquireAndProcess() { val resource acquireResource() try { resource.process() } finally { withContext(NonCancellable) { resource.release() // 即使被取消也始终释放 } } }十、委托属性的懒加载、可观察与 Map 映射10.1 属性委托标准库内置三类高频委托lazy懒初始化、Delegates.observable变化监听、by map把属性映射到 Map 键值常用于反序列化配置对象// 懒初始化 val expensiveData: ListUser by lazy { userRepository.findAll() } // 可观察属性 var name: String by Delegates.observable(initial) { _, old, new - logger.info(Name changed from $old to $new) } // Map 支撑的属性 class Config(private val map: MapString, Any?) { val host: String by map val port: Int by map val debug: Boolean by map } val config Config(mapOf(host to localhost, port to 8080, debug to true))10.2 接口委托无继承地复用实现并局部增强class LoggingUserRepository( private val delegate: UserRepository, private val logger: Logger, ) : UserRepository by delegate { // 只需覆写需要加日志的方法 override suspend fun findById(id: String): User? { logger.info(Finding user by id: $id) return delegate.findById(id).also { logger.info(Found user: ${it?.name ?: null}) } } }接口委托by delegate让“装饰器/代理”类只覆写需要增强的方法其余方法零样板透传。这是仓库评审规范中“无继承复用”主题的标准落地方式之一。十一、类型安全 DSLDslMarker与 lambda 接收者DSL 的精髓是 lambda 接收者 受限作用域接收者类型把可调用成员限定在 DSL 语义之内DslMarker阻止隐式外层接收者成员被误调用从而避免内外层 DSL 成员串味。11.1 HTML 风格 DSLDslMarker annotation class HtmlDsl HtmlDsl class HTML { private val children mutableListOfElement() fun head(init: Head.() - Unit) { children Head().apply(init) } fun body(init: Body.() - Unit) { children Body().apply(init) } override fun toString(): String children.joinToString(\n) } fun html(init: HTML.() - Unit): HTML HTML().apply(init) // 使用 val page html { head { title(My Page) } body { h1(Welcome) p(Hello, World!) } }11.2 配置型 DSLBuilder 数据类收口工程中更常见的是“配置 DSL”其模式可拆为三步Builder持有可变中间态 →fun x(init: XBuilder.() - Unit)构建入口 →build()产出不可变数据类。data class ServerConfig( val host: String 0.0.0.0, val port: Int 8080, val ssl: SslConfig? null, val database: DatabaseConfig? null, ) data class SslConfig(val certPath: String, val keyPath: String) data class DatabaseConfig(val url: String, val maxPoolSize: Int 10) class ServerConfigBuilder { var host: String 0.0.0.0 var port: Int 8080 private var ssl: SslConfig? null private var database: DatabaseConfig? null fun ssl(certPath: String, keyPath: String) { ssl SslConfig(certPath, keyPath) } fun database(url: String, maxPoolSize: Int 10) { database DatabaseConfig(url, maxPoolSize) } fun build(): ServerConfig ServerConfig(host, port, ssl, database) } fun serverConfig(init: ServerConfigBuilder.() - Unit): ServerConfig ServerConfigBuilder().apply(init).build() // 使用 val config serverConfig { host 0.0.0.0 port 443 ssl(/certs/cert.pem, /certs/key.pem) database(jdbc:postgresql://localhost:5432/mydb, maxPoolSize 20) }仓库规约里也存在完全同构的应用rules/kotlin/patterns.md的“Builder Pattern with DSL”把HttpClientConfig交给fun httpClient(block: HttpClientConfig.() - Unit)构建从而让baseUrl、timeout、interceptor { }以声明式写法出现在调用点。也就是说同样的模式既出现在本技能文档也贯穿于仓库的架构规约说明它是 Kotlin 生态“声明式 API”的事实标准。十二、Sequence多条链式操作的惰性求值对元素量巨大且操作链条多的集合普通List的每一步中间操作都会物化出整份集合改用asSequence()后各操作以元素为单位纵向流水执行filter→map→filter→take 各阶段按元素叠加在一个循环里配合无限序列与yield可以表达“按需生成”的数据源// Good: 大集合 多操作时使用 sequence val result users.asSequence() .filter { it.isActive } .map { it.email } .filter { it.endsWith(company.com) } .take(10) .toList() // Good: 无限序列 val fibonacci: SequenceLong sequence { var a 0L var b 1L while (true) { yield(a) val next a b a b b next } } val first20 fibonacci.take(20).toList()选择依据的简化记忆集合规模小或只有一两次操作时List的开销反而更低链条长、数据量大、或存在take提前终止时换Sequence收益明显。十三、Gradle Kotlin DSL用类型安全配置替代 Groovybuild.gradle.kts让构建脚本获得 IDE 补全、编译期校验与类型安全。技能文档给出了一套相对完整的服务端工程基线覆盖 Ktor、Exposed、Koin、协程与 Kotest/MockK 测试栈。注意示例中的版本号是撰写该文档时的基线落地时请对照官方版本发布页核对最新版本plugins { kotlin(jvm) version 2.3.10 kotlin(plugin.serialization) version 2.3.10 id(io.ktor.plugin) version 3.4.0 id(org.jetbrains.kotlinx.kover) version 0.9.7 id(io.gitlab.arturbosch.detekt) version 1.23.8 } group com.example version 1.0.0 kotlin { jvmToolchain(21) } dependencies { // Ktor implementation(io.ktor:ktor-server-core:3.4.0) implementation(io.ktor:ktor-server-netty:3.4.0) implementation(io.ktor:ktor-server-content-negotiation:3.4.0) implementation(io.ktor:ktor-serialization-kotlinx-json:3.4.0) // Exposed implementation(org.jetbrains.exposed:exposed-core:1.0.0) implementation(org.jetbrains.exposed:exposed-dao:1.0.0) implementation(org.jetbrains.exposed:exposed-jdbc:1.0.0) implementation(org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0) // Koin implementation(io.insert-koin:koin-ktor:4.2.0) // Coroutines implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2) // Testing testImplementation(io.kotest:kotest-runner-junit5:6.1.4) testImplementation(io.kotest:kotest-assertions-core:6.1.4) testImplementation(io.kotest:kotest-property:6.1.4) testImplementation(io.mockk:mockk:1.14.9) testImplementation(io.ktor:ktor-server-test-host:3.4.0) testImplementation(org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2) } tasks.withTypeTest { useJUnitPlatform() } detekt { config.setFrom(files(config/detekt/detekt.yml)) buildUponDefaultConfig true }配套评审视角见 agents/kotlin-reviewer.md 的 Gradle Build 检查项提醒三件事优先用libs.versions.toml版本目录集中管理版本号、删除未被使用的依赖、KMP 工程能写进commonMain的代码不要下沉到平台源集。十四、错误处理Result与前置条件断言14.1 用Result表达领域操作可恢复的领域失败建议用runCatching 链式map/getOrElse表达而不是抛异常suspend fun createUser(request: CreateUserRequest): ResultUser runCatching { require(request.name.isNotBlank()) { Name cannot be blank } require( in request.email) { Invalid email format } val user User( id UserId(UUID.randomUUID().toString()), name request.name, email Email(request.email), ) userRepository.save(user) user } val displayName createUser(request) .map { it.name } .getOrElse { Unknown }该写法与第 6 节的自定义密封Result互为两种取舍内置Result快捷但错误信息能力弱自建密封层级穷尽但需维护更多类型。仓库规约rules/kotlin/patterns.md对 Repository 的约定是suspend函数返回ResultT或自定义错误类型与本节一致。14.2require与check两种断言分工fun withdraw(account: Account, amount: Money): Account { require(amount.value 0) { Amount must be positive: $amount } // 入参校验 check(account.balance amount) { Insufficient balance: ${account.balance} $amount } // 状态校验 return account.copy(balance account.balance - amount) }语义区分require校验调用方传入的参数失败抛IllegalArgumentExceptioncheck校验当前对象/系统的内部状态失败抛IllegalStateException。二者都支持带消息的 lambda可写可读的失败上下文。十五、集合操作用管道代替手写循环filter→sortedBy→map链、groupBy、associateBy、partition几乎覆盖日常数据处理且都能以解构接收结果// Good: 链式操作 val activeAdminEmails: ListString users .filter { it.role Role.ADMIN it.isActive } .sortedBy { it.name } .map { it.email } // Good: 分组与聚合 val usersByRole: MapRole, ListUser users.groupBy { it.role } val oldestByRole: MapRole, User? users.groupBy { it.role } .mapValues { (_, users) - users.minByOrNull { it.createdAt } } // Good: associate 建 Map val usersById: MapUserId, User users.associateBy { it.id } // Good: partition 拆分 val (active, inactive) users.partition { it.isActive }注意users.partition { ... }用Pair解构同时拿到两个列表避免二次遍历。对“性能敏感且链条长”的同类场景记得切到第 12 节的asSequence()。十六、惯用法速查表惯用法说明valovervar优先不可变变量data class值对象自带 equals/hashCode/copysealed class/interface受限类型层级value class零开销的类型安全包装表达式when穷尽模式匹配安全调用?.空安全的成员访问Elvis?:可空默认值let/apply/also/run/with作用域函数扩展函数无需继承即可加行为copy()数据类的不可变更新require/check前置条件断言协程async/await结构化并发执行Flow冷响应式流sequence惰性求值委托by无继承复用实现十七、反模式清单评审一票否决项速览// Bad: 强解包可空类型 val name user!!.name // Bad: 平台类型泄漏来自 Java 的值未判空 fun getLength(s: String) s.length // 安全 fun getLength(s: String?) s?.length ?: 0 // 对 Java 传值要判空 // Bad: 可变数据类 data class MutableUser(var name: String, var email: String) // Bad: 用异常做控制流 try { val user findUser(id) } catch (e: NotFoundException) { // 预期内的情况不该走异常 } // Good: 改用可空返回或 Result val user: User? findUserOrNull(id) // Bad: 忽略协程作用域 GlobalScope.launch { /* 避免 GlobalScope */ } // Good: 使用结构化并发 coroutineScope { launch { /* 作用域受控 */ } } // Bad: 深度嵌套作用域函数 user?.let { u - u.address?.let { a - a.city?.let { c - process(c) } } } // Good: 直接空安全链 user?.address?.city?.let { process(it) }这些反模式在仓库中被进一步细化为可按严重级别上报的检查项见 agents/kotlin-reviewer.md 的检查清单GlobalScope使用、捕获CancellationException、StateFlow 内存放可变集合、init{}中无作用域收集 Flow、缺失WhileSubscribed等属于 HIGH 级——按评审规则只要出现即触发 block 阻断合入。可以据此搭建你自己的提交前 Kotlin 自检清单。十八、把模式落到测试与工具链模式文档讲的“应该怎么写”最终要靠测试与静态分析锁住。仓库提供了配套约束测试技术栈rules/kotlin/testing.mdKMP 用kotlin.testAndroid 用 JUnit 4/5流与状态用Turbine的viewModel.state.test { awaitItem() ... }断言状态序列协程用kotlinx-coroutines-test的runTestTestScope配合advanceUntilIdle()虚拟时间推进网络层用 KtorMockEngine按路径返回桩响应数据库用JdbcSqliteDriver(IN_MEMORY)/ Room 内存库测试命名反引号包裹的自然语言式用例名如 search with empty query returns all items是仓库明确推荐的风格Fake 优先于 Mock手写FakeItemRepository内部持MutableStateFlow通过emit()注入数据比 MockK 静态打桩更易读、更贴近真实行为静态质量门禁工程基线的detekt插件见第 13 节配合config/detekt/detekt.yml统一风格Kover 输出覆盖率二者共同充当“让编译器与工具帮你把关”的落地手段。结语让编译器成为你的第一道防线回到本文的起点Kotlin 代码应该简洁但可读。可空类型 安全调用消灭了一整类 NPEval、data class、不可变集合与copy()让状态迁移显式化密封类型把错误建模为可穷尽的类型而非随手抛出的异常协程的coroutineScope/supervisorScope与Flow提供了可取消、可清理的并发模型DslMarker与 lambda 接收者让配置 API 声明化Gradle Kotlin DSL 让构建脚本同样享受类型安全。当你对取舍感到不确定时记住技能文档的结语——尽量让编译器帮你用不可空类型消灭空指针、用穷尽when消灭漏分支、用结构化并发消灭悬挂协程。这套技能在 ECC 仓库中作为工程规约被kotlin-reviewer等 Agent 直接消费也值得在任何 Kotlin/Android/KMP 工程中作为编码与评审的准绳。【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考