Claude Agent Skill · by Affaan M

Kotlin Patterns

Install Kotlin Patterns skill for Claude Code from affaan-m/everything-claude-code.

Install
Terminal · npx
$npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-native-skills
Works with Paperclip

How Kotlin Patterns fits into a Paperclip company.

Kotlin Patterns drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md711 lines
Expand
---name: kotlin-patternsdescription: Idiomatic Kotlin patterns, best practices, and conventions for building robust, efficient, and maintainable Kotlin applications with coroutines, null safety, and DSL builders.origin: ECC--- # 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:**```kotlinfun getUserEmail(userId: String): String {    val user = userRepository.findById(userId)    return user?.email ?: "unknown@example.com"}``` **Sealed class for exhaustive results:**```kotlinsealed 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:**```kotlinsuspend 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 ?: "unknown@example.com"} // 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 ```kotlinsealed 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 = "alice@example.com"} // 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 ```kotlindata 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 | Idiom | Description ||-------|-------------|| `val` over `var` | Prefer immutable variables || `data class` | For value objects with equals/hashCode/copy || `sealed class/interface` | For restricted type hierarchies || `value class` | For type-safe wrappers with zero overhead || Expression `when` | Exhaustive pattern matching || Safe call `?.` | Null-safe member access || Elvis `?:` | Default value for nullables || `let`/`apply`/`also`/`run`/`with` | Scope functions for clean code || Extension functions | Add behavior without inheritance || `copy()` | Immutable updates on data classes || `require`/`check` | Precondition assertions || Coroutine `async`/`await` | Structured concurrent execution || `Flow` | Cold reactive streams || `sequence` | Lazy evaluation || Delegation `by` | Reuse 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.