Install
Terminal · npx$
npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-native-skillsWorks 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 packSource file
SKILL.md711 linesExpandCollapse
---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.Related skills
Agent Eval
Install Agent Eval skill for Claude Code from affaan-m/everything-claude-code.
Agent Harness Construction
Install Agent Harness Construction skill for Claude Code from affaan-m/everything-claude-code.
Agent Payment X402
Install Agent Payment X402 skill for Claude Code from affaan-m/everything-claude-code.