Communitygithub.com

Kotlin 开发模式

提供惯用的 Kotlin 模式、最佳实践和约定,用于构建健壮、高效且可维护的 Kotlin 应用,涵盖协程、空安全及 DSL 构建器。在编写或审查 Kotlin 代码,以及处理惯用结构和空安全问题时使用。

Kotlin 开发模式 是什么?

该技能为开发者提供了全面的 Kotlin 惯用模式指南,涵盖七个关键领域:利用类型系统和安全调用操作符实现空安全、通过 `val` 和 `copy()` 方法确保不可变性、使用密封类和接口构建穷举类型层次结构、借助协程和 `Flow` 实现结构化并发、通过扩展函数在不使用继承的情况下添加行为、使用 `@DslMarker` 和 lambda 接收者构建类型安全的 DSL,以及采用 Gradle Kotlin DSL 进行构建配置。该技能支持所有主要的 Kotlin 开发场景,包括编写新代码、审查现有代码、重构遗留代码、设计模块或库,以及配置 Gradle 构建脚本。通过遵循这些原则,开发者可以编写更简洁、更安全且更易于维护的 Kotlin 代码。示例展示了如何使用 Elvis 操作符进行空安全处理、使用密封类处理穷举性结果,以及使用 `async/await` 进行结构化并发。该技能特别强调避免反模式,如强制解包可空类型、过度使用可变状态以及嵌套作用域函数。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/kotlin-patterns

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Kotlin 开发模式 是做什么的?

用于构建健壮、高效且可维护的应用的 Kotlin 惯用模式与最佳实践。

使用场景

  • 编写新的 Kotlin 代码
  • 审查 Kotlin 代码
  • 重构现有的 Kotlin 代码
  • 设计 Kotlin 模块或库
  • 配置 Gradle Kotlin DSL 构建

工作原理

此技能在七个关键领域贯彻 Kotlin 惯用约定:利用类型系统与安全调用操作符(safe-call operators)实现空安全;通过 val 和数据类上的 copy() 方法确保不可变性;使用密封类与接口构造穷举(exhaustive)类型层次结构;通过协程与 Flow 实现结构化并发;利用扩展函数在不使用继承的情况下添加行为;使用 @DslMarker 与 lambda 接收者构建类型安全的 DSL 构建器;以及使用 Gradle Kotlin DSL 进行构建配置。

示例

使用 Elvis 操作符实现空安全:

fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

使用密封类处理穷举性结果:

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: AppError) : Result<Nothing>()
    data object Loading : Result<Nothing>()
}

使用 async/await 实现结构化并发:

suspend fun fetchUserWithPosts(userId: String): UserProfile =
    coroutineScope {
        val user = async { userService.getUser(userId) }
        val posts = async { postService.getUserPosts(userId) }
        UserProfile(user = user.await(), posts = posts.await())
    }

核心原则

1. 空安全

Kotlin 的类型系统区分了可空类型与非可空类型。请充分利用它。

// 良好实践:默认使用非可空类型
fun getUser(id: String): User {
    return userRepository.findById(id)
        ?: throw UserNotFoundException("未找到用户 $id")
}

// 良好实践:安全调用与 Elvis 操作符
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

// 不良实践:强制解包可空类型
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user!!.email // 若为 null 则抛出 NPE
}

2. 默认不可变性

优先使用 val 而非 var,优先使用不可变集合而非可变集合。

// 良好实践:不可变数据
data class User(
    val id: String,
    val name: String,
    val email: String,
)

// 良好实践:使用 copy() 进行转换
fun updateEmail(user: User, newEmail: String): User =
    user.copy(email = newEmail)

// 良好实践:不可变集合
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }

// 不良实践:可变状态
var currentUser: User? = null // 避免使用可变全局状态
val mutableUsers = mutableListOf<User>() // 除非确实需要,否则避免使用

3. 表达式体与单表达式函数

使用表达式体编写简洁、可读的函数。

// 良好实践:表达式体
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('@') }

// 良好实践:将 when 用作表达式
fun statusMessage(code: Int): String = when (code) {
    200 -> "OK"
    404 -> "Not Found"
    500 -> "Internal Server Error"
    else -> "未知状态码: $code"
}

// 不良实践:不必要的块体
fun isAdult(age: Int): Boolean {
    return age >= 18
}

4. 使用数据类表示值对象

对于主要用于保存数据的类型,使用数据类。

// 良好实践:自动提供 copy、equals、hashCode、toString 的数据类
data class CreateUserRequest(
    val name: String,
    val email: String,
    val role: Role = Role.USER,
)

// 良好实践:为类型安全使用内联值类(运行时零开销)
@JvmInline
value class UserId(val value: String) {
    init {
        require(value.isNotBlank()) { "UserId 不可为空" }
    }
}

@JvmInline
value class Email(val value: String) {
    init {
        require('@' in value) { "无效的电子邮件地址: $value" }
    }
}

fun getUser(id: UserId): User = userRepository.findById(id)

密封类与接口

建模受限的层次结构

// 良好实践:用于穷举性 when 的密封类
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: AppError) : Result<Nothing>()
    data object Loading : Result<Nothing>()
}

fun <T> Result<T>.getOrNull(): T? = when (this) {
    is Result.Success -> data
    is Result.Failure -> null
    is Result.Loading -> null
}

fun <T> Result<T>.getOrThrow(): T = when (this) {
    is Result.Success -> data
    is Result.Failure -> throw error.toException()
    is Result.Loading -> throw IllegalStateException("仍在加载")
}

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

作用域函数

何时使用哪种

// let: 转换可空值或作用域内的结果
val length: Int? = name?.let { it.trim().length }

// apply: 配置对象(返回对象本身)
val user = User().apply {
    name = "Alice"
    email = "[email protected]"
}

// also: 执行副作用(返回对象本身)
val user = createUser(request).also { logger.info("已创建用户: ${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()
}

反模式

// 不良实践:嵌套作用域函数
user?.let { u ->
    u.address?.let { addr ->
        addr.city?.let { city ->
            println(city) // 难以阅读
        }
    }
}

// 良好实践:改用链式安全调用
val city = user?.address?.city
city?.let { println(it) }

扩展函数

无需继承即可添加功能

// 良好实践:领域特定扩展
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()

// 良好实践:集合扩展
fun <T> List<T>.second(): T = this[1]

fun <T> List<T>.secondOrNull(): T? = getOrNull(1)

// 良好实践:限定作用域的扩展(不污染全局命名空间)
class UserService {
    private fun User.isActive(): Boolean =
        status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS))

    fun getActiveUsers(): List<User> = userRepository.findAll().filter { it.isActive() }
}

协程

结构化并发

// 良好实践:使用 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(),
        )
    }

// 良好实践:当子协程可以独立失败时使用 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()
            },
        )
    }

使用 Flow 处理响应式流

// 良好实践:带有恰当错误处理的冷流
fun observeUsers(): Flow<List<User>> = flow {
    while (currentCoroutineContext().isActive) {
        val users = userRepository.findAll()
        emit(users)
        delay(5.seconds)
    }
}.catch { e ->
    logger.error("观察用户时出错", e)
    emit(emptyList())
}

// 良好实践:Flow 操作符
fun searchUsers(query: Flow<String>): Flow<List<User>> =
    query
        .debounce(300.milliseconds)
        .distinctUntilChanged()
        .filter { it.length >= 2 }
        .mapLatest { q -> userRepository.search(q) }
        .catch { emit(emptyList()) }

取消与清理

// 良好实践:尊重取消
suspend fun processItems(items: List<Item>) {
    items.forEach { item ->
        ensureActive() // 在执行昂贵操作前检查取消
        processItem(item)
    }
}

// 良好实践:使用 try/finally 进行清理
suspend fun acquireAndProcess() {
    val resource = acquireResource()
    try {
        resource.process()
    } finally {
        withContext(NonCancellable) {
            resource.release() // 即使取消也要确保释放
        }
    }
}

委托

属性委托

// 延迟初始化
val expensiveData: List<User> by lazy {
    userRepository.findAll()
}

// 可观察属性
var name: String by Delegates.observable("initial") { _, old, new ->
    logger.info("名称从 '$old' 更改为 '$new'")
}

// 基于 Map 的属性
class Config(private val map: Map<String, 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))

接口委托

// 良好实践:委托接口实现
class LoggingUserRepository(
    private val delegate: UserRepository,
    private val logger: Logger,
) : UserRepository by delegate {
    // 仅需重写需要添加日志的方法
    override suspend fun findById(id: String): User? {
        logger.info("正在按 id 查找用户: $id")
        return delegate.findById(id).also {
            logger.info("找到用户: ${it?.name ?: "null"}")
        }
    }
}

DSL 构建器

类型安全的构建器

// 良好实践:使用 @DslMarker 的 DSL
@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HTML {
    private val children = mutableListOf<Element>()

    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("我的页面") }
    body {
        h1("欢迎")
        p("你好,世界!")
    }
}

配置 DSL

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)
}

用于惰性求值的序列

// 良好实践:对于包含多个操作的大型集合,使用序列
val result = users.asSequence()
    .filter { it.isActive }
    .map { it.email }
    .filter { it.endsWith("@company.com") }
    .take(10)
    .toList()

// 良好实践:生成无限序列
val fibonacci: Sequence<Long> = 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()

Gradle Kotlin DSL

build.gradle.kts 配置

// 检查最新版本:https://kotlinlang.org/docs/releases.html
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.")
}

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能