Kotlin Patterns

affaan-m/ECC/.kiro/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.

AI 產生的概覽

用於撰寫、審查與重構 Kotlin 程式碼的慣用 Kotlin 模式與最佳實務。

功能
提供慣用 Kotlin 慣例的參考,並附程式碼範例,涵蓋空值安全、不可變性、資料類別與值類別、密封階層、作用域函式、擴充函式、協同程式與 Flow、委派、DSL 建構器、序列、Gradle Kotlin DSL 設定以及錯誤處理。它也列出應避免的反模式,例如強制解開可空型別、可變資料類別與 GlobalScope 的使用。產出是指導說明與範例程式碼,而非產生的檔案。
適用情境
適用於撰寫新的 Kotlin 程式碼、審查或重構現有 Kotlin 程式碼、設計 Kotlin 模組或函式庫,或設定 Gradle Kotlin DSL 建置時。
執行需求
除代理本身外無需其他條件;僅為說明性內容,不需要指令碼、套件或認證資訊。

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位於.kiro/skills/kotlin-patterns提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架