$npx -y skills add softspark/ai-toolkit --skill kotlin-patternsKotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx.
| 1 | # Kotlin Patterns Skill |
| 2 | |
| 3 | ## Project Structure |
| 4 | |
| 5 | ### Gradle KTS Multi-Module Layout |
| 6 | ``` |
| 7 | project-root/ |
| 8 | ├── build.gradle.kts |
| 9 | ├── settings.gradle.kts |
| 10 | ├── gradle/libs.versions.toml |
| 11 | ├── app/ |
| 12 | │ ├── build.gradle.kts |
| 13 | │ └── src/{main,test}/kotlin/com/example/app/ |
| 14 | ├── domain/ |
| 15 | │ └── src/main/kotlin/com/example/domain/ |
| 16 | │ ├── model/ |
| 17 | │ ├── repository/ |
| 18 | │ └── usecase/ |
| 19 | └── infrastructure/ |
| 20 | └── src/main/kotlin/com/example/infra/ |
| 21 | ``` |
| 22 | |
| 23 | ### settings.gradle.kts |
| 24 | ```kotlin |
| 25 | rootProject.name = "my-project" |
| 26 | dependencyResolutionManagement { |
| 27 | versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } } |
| 28 | } |
| 29 | include(":app", ":domain", ":infrastructure") |
| 30 | ``` |
| 31 | |
| 32 | ### Module build.gradle.kts |
| 33 | ```kotlin |
| 34 | plugins { |
| 35 | alias(libs.plugins.kotlin.jvm) |
| 36 | alias(libs.plugins.kotlin.serialization) |
| 37 | } |
| 38 | dependencies { |
| 39 | implementation(project(":domain")) |
| 40 | implementation(libs.kotlinx.coroutines.core) |
| 41 | testImplementation(libs.bundles.testing) |
| 42 | } |
| 43 | kotlin { jvmToolchain(21) } |
| 44 | ``` |
| 45 | |
| 46 | --- |
| 47 | |
| 48 | ## Idioms / Code Style |
| 49 | |
| 50 | ### Data Classes + Value Classes |
| 51 | ```kotlin |
| 52 | data class User( |
| 53 | val id: UserId, |
| 54 | val name: String, |
| 55 | val email: String, |
| 56 | val role: Role = Role.USER, |
| 57 | ) { |
| 58 | init { |
| 59 | require(name.isNotBlank()) { "Name must not be blank" } |
| 60 | require(email.contains("@")) { "Invalid email format" } |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | @JvmInline |
| 65 | value class UserId(val value: String) // Zero-overhead type-safe ID |
| 66 | enum class Role { ADMIN, USER, GUEST } |
| 67 | ``` |
| 68 | |
| 69 | ### Sealed Interfaces for Domain Modeling |
| 70 | ```kotlin |
| 71 | sealed interface PaymentResult { |
| 72 | data class Success(val transactionId: String, val amount: Money) : PaymentResult |
| 73 | data class Declined(val reason: String) : PaymentResult |
| 74 | data class Error(val exception: Throwable) : PaymentResult |
| 75 | } |
| 76 | |
| 77 | // Exhaustive when -- compiler enforces all branches |
| 78 | fun handlePayment(result: PaymentResult): String = when (result) { |
| 79 | is PaymentResult.Success -> "Paid: ${result.amount}" |
| 80 | is PaymentResult.Declined -> "Declined: ${result.reason}" |
| 81 | is PaymentResult.Error -> "Error: ${result.exception.message}" |
| 82 | } |
| 83 | ``` |
| 84 | |
| 85 | ### Extension Functions |
| 86 | ```kotlin |
| 87 | fun String.toSlug(): String = |
| 88 | lowercase().replace(Regex("[^a-z0-9\\s-]"), "").replace(Regex("\\s+"), "-").trim('-') |
| 89 | |
| 90 | // Scoped extensions -- visible only inside containing class |
| 91 | class OrderService { |
| 92 | private fun Order.totalWithTax(): Money = total * (1 + taxRate) |
| 93 | } |
| 94 | ``` |
| 95 | |
| 96 | ### Null Safety |
| 97 | ```kotlin |
| 98 | fun getDisplayName(user: User?): String = |
| 99 | user?.name?.takeIf { it.isNotBlank() } ?: "Anonymous" |
| 100 | |
| 101 | fun processEmail(email: String?) { email?.let { sendWelcomeEmail(it) } } |
| 102 | |
| 103 | fun loadConfig(path: String?): Config { |
| 104 | val resolved = requireNotNull(path) { "Config path must not be null" } |
| 105 | return parseConfig(resolved) |
| 106 | } |
| 107 | ``` |
| 108 | |
| 109 | ### Scope Functions |
| 110 | |
| 111 | | Function | Ref | Returns | Use case | |
| 112 | |----------|-----|---------|----------| |
| 113 | | `let` | `it` | Lambda result | Null check + transform | |
| 114 | | `run` | `this` | Lambda result | Config + compute | |
| 115 | | `apply` | `this` | Object itself | Object initialization | |
| 116 | | `also` | `it` | Object itself | Side effects | |
| 117 | |
| 118 | ```kotlin |
| 119 | val conn = Connection().apply { host = "localhost"; port = 5432 } |
| 120 | fun createUser(req: CreateUserRequest): User = |
| 121 | userRepository.save(req.toUser()).also { logger.info("Created: ${it.id}") } |
| 122 | ``` |
| 123 | |
| 124 | ### Type-Safe Builders (DSL) |
| 125 | ```kotlin |
| 126 | fun html(block: HtmlBuilder.() -> Unit): String = HtmlBuilder().apply(block).build() |
| 127 | |
| 128 | val page = html { |
| 129 | head { title("My Page") } |
| 130 | body { p("Hello, world!") } |
| 131 | } |
| 132 | ``` |
| 133 | |
| 134 | --- |
| 135 | |
| 136 | ## Error Handling |
| 137 | |
| 138 | ### Result<T> and runCatching |
| 139 | ```kotlin |
| 140 | fun findUser(id: UserId): Result<User> = runCatching { |
| 141 | userRepository.findById(id) ?: throw UserNotFoundException(id) |
| 142 | } |
| 143 | |
| 144 | fun getUserDisplayName(id: UserId): String = |
| 145 | findUser(id).map { it.name }.recover { "Unknown User" }.getOrThrow() |
| 146 | |
| 147 | fun handleLookup(id: UserId): Response = findUser(id).fold( |
| 148 | onSuccess = { Response.ok(it) }, |
| 149 | onFailure = { Response.notFound(it.message) }, |
| 150 | ) |
| 151 | ``` |
| 152 | |
| 153 | ### Sealed Class Error Hierarchy |
| 154 | ```kotlin |
| 155 | sealed class DomainError(override val message: String) : Exception(message) { |
| 156 | data class NotFound(val resource: String, val id: String) : DomainError("$resource not found: $id") |
| 157 | data class Validation(val field: String, val reason: String) : DomainError("Invalid $field: $reason") |
| 158 | data class Conflict(val detail: String) : DomainError("Conflict: $detail") |
| 159 | } |
| 160 | |
| 161 | fun handleError(error: DomainError): Response = when (error) { |
| 162 | is DomainError.NotFound -> Response.status(404).body(error.message) |
| 163 | is DomainError.Validation -> Response.status(422).body(error.message) |
| 164 | is DomainError.Conflict -> Response.status(409).body(error.message) |
| 165 | } |
| 166 | ``` |
| 167 | |
| 168 | ### Preconditions |
| 169 | ```kotlin |
| 170 | fun transferMoney(from: Account, to: Account, amount: Money) { |