Skip to content
Development
Skill

/kotlin-patterns

Kotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx.

From plugin
ai-toolkit
161111 skills44 agents
Install
$ npx -y skills add softspark/ai-toolkit --skill kotlin-patterns --agent claude-code

How 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.md
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("E
Read more
Ships withai-toolkit

Professional-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,

Get the whole plugin