/kotlin-patterns
Kotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx.
$ npx -y skills add softspark/ai-toolkit --skill kotlin-patterns --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/kotlin-patterns
Context preview
The summary Claude sees to decide when to auto-load this skill.
Kotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx.
SKILL.md
kotlin-patterns.SKILL.mdname: kotlin-patterns
description: "Kotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx."
effort: medium
user-invocable: false
allowed-tools: Read
Kotlin Patterns Skill
Project Structure
Gradle KTS Multi-Module Layout
project-root/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/libs.versions.toml
├── app/
│ ├── build.gradle.kts
│ └── src/{main,test}/kotlin/com/example/app/
├── domain/
│ └── src/main/kotlin/com/example/domain/
│ ├── model/
│ ├── repository/
│ └── usecase/
└── infrastructure/
└── src/main/kotlin/com/example/infra/settings.gradle.kts
rootProject.name = "my-project"
dependencyResolutionManagement {
versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } }
}
include(":app", ":domain", ":infrastructure")Module build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(project(":domain"))
implementation(libs.kotlinx.coroutines.core)
testImplementation(libs.bundles.testing)
}
kotlin { jvmToolchain(21) }---
Idioms / Code Style
Data Classes + Value Classes
data class User(
val id: UserId,
val name: String,
val email: String,
val role: Role = Role.USER,
) {
init {
require(name.isNotBlank()) { "Name must not be blank" }
require(email.contains("@")) { "Invalid email format" }
}
}
@JvmInline
value class UserId(val value: String) // Zero-overhead type-safe ID
enum class Role { ADMIN, USER, GUEST }Sealed Interfaces for Domain Modeling
sealed interface PaymentResult {
data class Success(val transactionId: String, val amount: Money) : PaymentResult
data class Declined(val reason: String) : PaymentResult
data class Error(val exception: Throwable) : PaymentResult
}
// Exhaustive when -- compiler enforces all branches
fun handlePayment(result: PaymentResult): String = when (result) {
is PaymentResult.Success -> "Paid: ${result.amount}"
is PaymentResult.Declined -> "Declined: ${result.reason}"
is PaymentResult.Error -> "Error: ${result.exception.message}"
}Extension Functions
fun String.toSlug(): String =
lowercase().replace(Regex("[^a-z0-9\\s-]"), "").replace(Regex("\\s+"), "-").trim('-')
// Scoped extensions -- visible only inside containing class
class OrderService {
private fun Order.totalWithTax(): Money = total * (1 + taxRate)
}Null Safety
fun getDisplayName(user: User?): String =
user?.name?.takeIf { it.isNotBlank() } ?: "Anonymous"
fun processEmail(email: String?) { email?.let { sendWelcomeEmail(it) } }
fun loadConfig(path: String?): Config {
val resolved = requireNotNull(path) { "Config path must not be null" }
return parseConfig(resolved)
}Scope Functions
| Function | Ref | Returns | Use case | |----------|-----|---------|----------| | `let` | `it` | Lambda result | Null check + transform | | `run` | `this` | Lambda result | Config + compute | | `apply` | `this` | Object itself | Object initialization | | `also` | `it` | Object itself | Side effects |
val conn = Connection().apply { host = "localhost"; port = 5432 }
fun createUser(req: CreateUserRequest): User =
userRepository.save(req.toUser()).also { logger.info("Created: ${it.id}") }Type-Safe Builders (DSL)
fun html(block: HtmlBuilder.() -> Unit): String = HtmlBuilder().apply(block).build()
val page = html {
head { title("My Page") }
body { p("Hello, world!") }
}---
Error Handling
Result<T> and runCatching
fun findUser(id: UserId): Result<User> = runCatching {
userRepository.findById(id) ?: throw UserNotFoundException(id)
}
fun getUserDisplayName(id: UserId): String =
findUser(id).map { it.name }.recover { "Unknown User" }.getOrThrow()
fun handleLookup(id: UserId): Response = findUser(id).fold(
onSuccess = { Response.ok(it) },
onFailure = { Response.notFound(it.message) },
)Sealed Class Error Hierarchy
sealed class DomainError(override val message: String) : Exception(message) {
data class NotFound(val resource: String, val id: String) : DomainError("$resource not found: $id")
data class Validation(val field: String, val reason: String) : DomainError("Invalid $field: $reason")
data class Conflict(val detail: String) : DomainError("Conflict: $detail")
}
fun handleError(error: DomainError): Response = when (error) {
is DomainError.NotFound -> Response.status(404).body(error.message)
is DomainError.Validation -> Response.status(422).body(error.message)
is DomainError.Conflict -> Response.status(409).body(error.message)
}Preconditions
fun transferMoney(from: Account, to: Account, amount: Money) {
require(amount.value > 0) { "Transfer amount must be positive" }
require(from.id != to.id) { "Cannot transfer to same account" }
check(from.balance >= amount) { "Insufficient funds: ${from.balance}" }
}---
Testing Patterns
JUnit 5 + MockK
class UserServiceTest {
private val repository = mockk<UserRepository>()
private val notifier = mockk<NotificationService>(relaxed = true)
private val service = UserService(repository, notifier)
@Test
fun `creates user and sends welcome notification`() {
val expected = User(UserId("1"), "Alice", "alice@test.com")
every { repository.save(any()) } returns expected
val result = service.createUser(CreateUserRequest("Alice", "alice@test.com"))
assertThat(result).isEqualTo(expected)
verify(exactly = 1) { notifier.sendWelcome(expected) }
}
@Test
fun `throws on duplicate email`() {
every { repository.save(any()) } throws DomainError.Conflict("ERead more
name: kotlin-patterns description: "Kotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx." effort: medium user-invocable: false allowed-tools: Read
Kotlin Patterns Skill
Project Structure
Gradle KTS Multi-Module Layout
project-root/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/libs.versions.toml
├── app/
│ ├── build.gradle.kts
│ └── src/{main,test}/kotlin/com/example/app/
├── domain/
│ └── src/main/kotlin/com/example/domain/
│ ├── model/
│ ├── repository/
│ └── usecase/
└── infrastructure/
└── src/main/kotlin/com/example/infra/settings.gradle.kts
rootProject.name = "my-project"
dependencyResolutionManagement {
versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } }
}
include(":app", ":domain", ":infrastructure")Module build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(project(":domain"))
implementation(libs.kotlinx.coroutines.core)
testImplementation(libs.bundles.testing)
}
kotlin { jvmToolchain(21) }---
Idioms / Code Style
Data Classes + Value Classes
data class User(
val id: UserId,
val name: String,
val email: String,
val role: Role = Role.USER,
) {
init {
require(name.isNotBlank()) { "Name must not be blank" }
require(email.contains("@")) { "Invalid email format" }
}
}
@JvmInline
value class UserId(val value: String) // Zero-overhead type-safe ID
enum class Role { ADMIN, USER, GUEST }Sealed Interfaces for Domain Modeling
sealed interface PaymentResult {
data class Success(val transactionId: String, val amount: Money) : PaymentResult
data class Declined(val reason: String) : PaymentResult
data class Error(val exception: Throwable) : PaymentResult
}
// Exhaustive when -- compiler enforces all branches
fun handlePayment(result: PaymentResult): String = when (result) {
is PaymentResult.Success -> "Paid: ${result.amount}"
is PaymentResult.Declined -> "Declined: ${result.reason}"
is PaymentResult.Error -> "Error: ${result.exception.message}"
}Extension Functions
fun String.toSlug(): String =
lowercase().replace(Regex("[^a-z0-9\\s-]"), "").replace(Regex("\\s+"), "-").trim('-')
// Scoped extensions -- visible only inside containing class
class OrderService {
private fun Order.totalWithTax(): Money = total * (1 + taxRate)
}Null Safety
fun getDisplayName(user: User?): String =
user?.name?.takeIf { it.isNotBlank() } ?: "Anonymous"
fun processEmail(email: String?) { email?.let { sendWelcomeEmail(it) } }
fun loadConfig(path: String?): Config {
val resolved = requireNotNull(path) { "Config path must not be null" }
return parseConfig(resolved)
}Scope Functions
| Function | Ref | Returns | Use case | |----------|-----|---------|----------| | `let` | `it` | Lambda result | Null check + transform | | `run` | `this` | Lambda result | Config + compute | | `apply` | `this` | Object itself | Object initialization | | `also` | `it` | Object itself | Side effects |
val conn = Connection().apply { host = "localhost"; port = 5432 }
fun createUser(req: CreateUserRequest): User =
userRepository.save(req.toUser()).also { logger.info("Created: ${it.id}") }Type-Safe Builders (DSL)
fun html(block: HtmlBuilder.() -> Unit): String = HtmlBuilder().apply(block).build()
val page = html {
head { title("My Page") }
body { p("Hello, world!") }
}---
Error Handling
Result<T> and runCatching
fun findUser(id: UserId): Result<User> = runCatching {
userRepository.findById(id) ?: throw UserNotFoundException(id)
}
fun getUserDisplayName(id: UserId): String =
findUser(id).map { it.name }.recover { "Unknown User" }.getOrThrow()
fun handleLookup(id: UserId): Response = findUser(id).fold(
onSuccess = { Response.ok(it) },
onFailure = { Response.notFound(it.message) },
)Sealed Class Error Hierarchy
sealed class DomainError(override val message: String) : Exception(message) {
data class NotFound(val resource: String, val id: String) : DomainError("$resource not found: $id")
data class Validation(val field: String, val reason: String) : DomainError("Invalid $field: $reason")
data class Conflict(val detail: String) : DomainError("Conflict: $detail")
}
fun handleError(error: DomainError): Response = when (error) {
is DomainError.NotFound -> Response.status(404).body(error.message)
is DomainError.Validation -> Response.status(422).body(error.message)
is DomainError.Conflict -> Response.status(409).body(error.message)
}Preconditions
fun transferMoney(from: Account, to: Account, amount: Money) {
require(amount.value > 0) { "Transfer amount must be positive" }
require(from.id != to.id) { "Cannot transfer to same account" }
check(from.balance >= amount) { "Insufficient funds: ${from.balance}" }
}---
Testing Patterns
JUnit 5 + MockK
class UserServiceTest {
private val repository = mockk<UserRepository>()
private val notifier = mockk<NotificationService>(relaxed = true)
private val service = UserService(repository, notifier)
@Test
fun `creates user and sends welcome notification`() {
val expected = User(UserId("1"), "Alice", "alice@test.com")
every { repository.save(any()) } returns expected
val result = service.createUser(CreateUserRequest("Alice", "alice@test.com"))
assertThat(result).isEqualTo(expected)
verify(exactly = 1) { notifier.sendWelcome(expected) }
}
@Test
fun `throws on duplicate email`() {
every { repository.save(any()) } throws DomainError.Conflict("EProfessional-grade AI coding toolkit with multi-platform support. Machine-enforced safety, 109 skills, 44 agents, expanded lifecycle hooks, persona presets, experimental opt-in plugin packs, and benchmark tooling — works with Claude Code, Claude Chat/Cowork,
Repo: softspark/ai-toolkit
Other skills on ai-toolkit.
- /ai-toolkit-rules
Mandatory engineering, security, testing, git, performance, quality, and response rules. Claude MUST load this skill for every technical, coding, debugging, review, architecture, DevOps, data, or file-editing task in Chat or Cowork.
Open skill - /mem-search
Search past coding sessions using natural language. Finds relevant observations, decisions, and context from previous work.
Open skill - /a11y-validate
Accessibility validator: WCAG 2.1 AA, EN 301 549, EAA. Triggers: a11y, accessibility, WCAG, EAA, ARIA, contrast, keyboard, screen reader.
Open skill - /agent-creator
Creates new specialized agents with frontmatter, tools, delegation. Triggers: new agent, create agent, agent scaffold, specialized agent.
Open skill - /analyze
Analyzes code quality, complexity, patterns across codebase. Triggers: quality report, hotspot scan, code analysis, architecture signal.
Open skill - /api-patterns
REST/GraphQL API design: naming, versioning, pagination, idempotency, OpenAPI. Triggers: API design, REST, GraphQL, OpenAPI, Swagger, idempotency, rate limit.
Open skill

