Kotlin Patterns

affaan-m/ECC/skills/kotlin-patterns

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775无许可证275K 个星标收录于 2026年10月9日更新于 2026年10月9日仓库4天前更新

Idiomatic Kotlin patterns, best practices, and conventions for building robust, efficient, and maintainable Kotlin applications with coroutines, null safety, and DSL builders. Use when writing or reviewing Kotlin code and idiomatic structure or null safety is in question.

AI 生成的概览

用于编写、审查和重构健壮且易维护的 Kotlin 代码的惯用模式与约定。

功能
提供 Kotlin 惯用约定的参考,涵盖空安全、不可变性、密封类型层次、作用域函数、扩展函数、协程与 Flow、委托、DSL 构建器、序列、Gradle Kotlin DSL 以及错误处理。每个主题都配有正例与反例代码、惯用法速查表和应避免的反模式清单。它产出的是指导与代码示例,而不是生成的文件或工具。
适用场景
适用于编写新的 Kotlin 代码、审查或重构现有 Kotlin 代码、设计 Kotlin 模块或库,或配置 Gradle Kotlin DSL 构建。当惯用结构、空安全或并发风格存在疑问时最为相关。
运行要求
无需脚本或工具,仅为说明性内容。示例涉及 Kotlin、协程、Flow、Gradle Kotlin DSL 以及 Ktor、Exposed、Koin、Kotest、MockK 等库,但阅读这些指导无需安装任何东西。

Kotlin Development Patterns

Idiomatic Kotlin patterns and best practices for building robust, efficient, and maintainable applications.

When to Use

  • Writing new Kotlin code
  • Reviewing Kotlin code
  • Refactoring existing Kotlin code
  • Designing Kotlin modules or libraries
  • Configuring Gradle Kotlin DSL builds

How It Works

This skill enforces idiomatic Kotlin conventions across seven key areas: null safety using the type system and safe-call operators, immutability via val and copy() on data classes, sealed classes and interfaces for exhaustive type hierarchies, structured concurrency with coroutines and Flow, extension functions for adding behaviour without inheritance, type-safe DSL builders using @DslMarker and lambda receivers, and Gradle Kotlin DSL for build configuration.

Examples

Null safety with Elvis operator:

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

Sealed class for exhaustive results:

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

Structured concurrency with async/await:

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

Core Principles

1. Null Safety

Kotlin's type system distinguishes nullable and non-nullable types. Leverage it fully.

kotlin
// Good: Use non-nullable types by defaultfun getUser(id: String): User {    return userRepository.findById(id)        ?: throw UserNotFoundException("User $id not found")}
// Good: Safe calls and Elvis operatorfun getUserEmail(userId: String): String {    val user = userRepository.findById(userId)    return user?.email ?: "[email protected]"}
// Bad: Force-unwrapping nullable typesfun getUserEmail(userId: String): String {    val user = userRepository.findById(userId)    return user!!.email // Throws NPE if null}

2. Immutability by Default

Prefer val over var, immutable collections over mutable ones.

kotlin
// Good: Immutable datadata class User(    val id: String,    val name: String,    val email: String,)
// Good: Transform with copy()fun updateEmail(user: User, newEmail: String): User =    user.copy(email = newEmail)
// Good: Immutable collectionsval users: List<User> = listOf(user1, user2)val filtered = users.filter { it.email.isNotBlank() }
// Bad: Mutable statevar currentUser: User? = null // Avoid mutable global stateval mutableUsers = mutableListOf<User>() // Avoid unless truly needed

3. Expression Bodies and Single-Expression Functions

Use expression bodies for concise, readable functions.

kotlin
// Good: Expression bodyfun 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 as expressionfun statusMessage(code: Int): String = when (code) {    200 -> "OK"    404 -> "Not Found"    500 -> "Internal Server Error"    else -> "Unknown status: $code"}
// Bad: Unnecessary block bodyfun isAdult(age: Int): Boolean {    return age >= 18}

4. Data Classes for Value Objects

Use data classes for types that primarily hold data.

kotlin
// Good: Data class with copy, equals, hashCode, toStringdata class CreateUserRequest(    val name: String,    val email: String,    val role: Role = Role.USER,)
// Good: Value class for type safety (zero overhead at runtime)@JvmInlinevalue class UserId(val value: String) {    init {        require(value.isNotBlank()) { "UserId cannot be blank" }    }}
@JvmInlinevalue class Email(val value: String) {    init {        require('@' in value) { "Invalid email: $value" }    }}
fun getUser(id: UserId): User = userRepository.findById(id)

Sealed Classes and Interfaces

Modeling Restricted Hierarchies

kotlin
// Good: Sealed class for exhaustive whensealed 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("Still loading")}

Sealed Interfaces for API Responses

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

Scope Functions

When to Use Each

kotlin
// let: Transform nullable or scoped resultval length: Int? = name?.let { it.trim().length }
// apply: Configure an object (returns the object)val user = User().apply {    name = "Alice"    email = "[email protected]"}
// also: Side effects (returns the object)val user = createUser(request).also { logger.info("Created user: ${it.id}") }
// run: Execute a block with receiver (returns result)val result = connection.run {    prepareStatement(sql)    executeQuery()}
// with: Non-extension form of runval csv = with(StringBuilder()) {    appendLine("name,email")    users.forEach { appendLine("${it.name},${it.email}") }    toString()}

Anti-Patterns

kotlin
// Bad: Nesting scope functionsuser?.let { u ->    u.address?.let { addr ->        addr.city?.let { city ->            println(city) // Hard to read        }    }}
// Good: Chain safe calls insteadval city = user?.address?.citycity?.let { println(it) }

Extension Functions

Adding Functionality Without Inheritance

kotlin
// Good: Domain-specific extensionsfun 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: Collection extensionsfun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
// Good: Scoped extensions (not polluting global namespace)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() }}

Coroutines

Structured Concurrency

kotlin
// Good: Structured concurrency with coroutineScopesuspend 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 when children can fail independentlysuspend 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 for Reactive Streams

kotlin
// Good: Cold flow with proper error handlingfun observeUsers(): Flow<List<User>> = flow {    while (currentCoroutineContext().isActive) {        val users = userRepository.findAll()        emit(users)        delay(5.seconds)    }}.catch { e ->    logger.error("Error observing users", e)    emit(emptyList())}
// Good: Flow operatorsfun searchUsers(query: Flow<String>): Flow<List<User>> =    query        .debounce(300.milliseconds)        .distinctUntilChanged()        .filter { it.length >= 2 }        .mapLatest { q -> userRepository.search(q) }        .catch { emit(emptyList()) }

Cancellation and Cleanup

kotlin
// Good: Respect cancellationsuspend fun processItems(items: List<Item>) {    items.forEach { item ->        ensureActive() // Check cancellation before expensive work        processItem(item)    }}
// Good: Cleanup with try/finallysuspend fun acquireAndProcess() {    val resource = acquireResource()    try {        resource.process()    } finally {        withContext(NonCancellable) {            resource.release() // Always release, even on cancellation        }    }}

Delegation

Property Delegation

kotlin
// Lazy initializationval expensiveData: List<User> by lazy {    userRepository.findAll()}
// Observable propertyvar name: String by Delegates.observable("initial") { _, old, new ->    logger.info("Name changed from '$old' to '$new'")}
// Map-backed propertiesclass 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))

Interface Delegation

kotlin
// Good: Delegate interface implementationclass LoggingUserRepository(    private val delegate: UserRepository,    private val logger: Logger,) : UserRepository by delegate {    // Only override what you need to add logging to    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"}")        }    }}

DSL Builders

Type-Safe Builders

kotlin
// Good: DSL with @DslMarker@DslMarkerannotation class HtmlDsl
@HtmlDslclass 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)
// Usageval page = html {    head { title("My Page") }    body {        h1("Welcome")        p("Hello, World!")    }}

Configuration DSL

kotlin
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()
// Usageval config = serverConfig {    host = "0.0.0.0"    port = 443    ssl("/certs/cert.pem", "/certs/key.pem")    database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20)}

Sequences for Lazy Evaluation

kotlin
// Good: Use sequences for large collections with multiple operationsval result = users.asSequence()    .filter { it.isActive }    .map { it.email }    .filter { it.endsWith("@company.com") }    .take(10)    .toList()
// Good: Generate infinite sequencesval 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 Configuration

kotlin
// Check for latest versions: https://kotlinlang.org/docs/releases.htmlplugins {    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.withType<Test> {    useJUnitPlatform()}
detekt {    config.setFrom(files("config/detekt/detekt.yml"))    buildUponDefaultConfig = true}

Error Handling Patterns

Result Type for Domain Operations

kotlin
// Good: Use Kotlin's Result or a custom sealed classsuspend fun createUser(request: CreateUserRequest): Result<User> = 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}
// Good: Chain resultsval displayName = createUser(request)    .map { it.name }    .getOrElse { "Unknown" }

require, check, error

kotlin
// Good: Preconditions with clear messagesfun 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)}

Collection Operations

Idiomatic Collection Processing

kotlin
// Good: Chained operationsval activeAdminEmails: List<String> = users    .filter { it.role == Role.ADMIN && it.isActive }    .sortedBy { it.name }    .map { it.email }
// Good: Grouping and aggregationval usersByRole: Map<Role, List<User>> = users.groupBy { it.role }
val oldestByRole: Map<Role, User?> = users.groupBy { it.role }    .mapValues { (_, users) -> users.minByOrNull { it.createdAt } }
// Good: Associate for map creationval usersById: Map<UserId, User> = users.associateBy { it.id }
// Good: Partition for splittingval (active, inactive) = users.partition { it.isActive }

Quick Reference: Kotlin Idioms

IdiomDescription
val over varPrefer immutable variables
data classFor value objects with equals/hashCode/copy
sealed class/interfaceFor restricted type hierarchies
value classFor type-safe wrappers with zero overhead
Expression whenExhaustive pattern matching
Safe call ?.Null-safe member access
Elvis ?:Default value for nullables
let/apply/also/run/withScope functions for clean code
Extension functionsAdd behavior without inheritance
copy()Immutable updates on data classes
require/checkPrecondition assertions
Coroutine async/awaitStructured concurrent execution
FlowCold reactive streams
sequenceLazy evaluation
Delegation byReuse implementation without inheritance

Anti-Patterns to Avoid

kotlin
// Bad: Force-unwrapping nullable typesval name = user!!.name
// Bad: Platform type leakage from Javafun getLength(s: String) = s.length // Safefun getLength(s: String?) = s?.length ?: 0 // Handle nulls from Java
// Bad: Mutable data classesdata class MutableUser(var name: String, var email: String)
// Bad: Using exceptions for control flowtry {    val user = findUser(id)} catch (e: NotFoundException) {    // Don't use exceptions for expected cases}
// Good: Use nullable return or Resultval user: User? = findUserOrNull(id)
// Bad: Ignoring coroutine scopeGlobalScope.launch { /* Avoid GlobalScope */ }
// Good: Use structured concurrencycoroutineScope {    launch { /* Properly scoped */ }}
// Bad: Deeply nested scope functionsuser?.let { u ->    u.address?.let { a ->        a.city?.let { c -> process(c) }    }}
// Good: Direct null-safe chainuser?.address?.city?.let { process(it) }

Remember: Kotlin code should be concise but readable. Leverage the type system for safety, prefer immutability, and use coroutines for concurrency. When in doubt, let the compiler help you.

来源与署名

来源:affaan-m/ECC位于skills/kotlin-patterns提交ef648e0

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架