Skip to main content

kotlin-patterns

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

소스 정보

저장소
softspark/ai-toolkit
최근 소스 활동
2026년 5월 6일 09:48
감지된 SKILL.md 언어
영어
스타
179
포크
21

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

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 ```kotlin rootProject.name = "my-project" dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } } } include(":app", ":domain", ":infrastructure") ``` ### Module build.gradle.kts ```kotlin 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 ```kotlin 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 ```kotlin 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 ```kotlin 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 ```kotlin 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 | ```kotlin 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) ```kotlin 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 ```kotlin 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 ```kotlin 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 ```kotlin 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 ```kotlin 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("Email exists") assertThrows<DomainError.Conflict> { service.createUser(CreateUserRequest("Bob", "dup@test.com")) } } } ``` ### Kotest + Property-Based Testing ```kotlin class MoneySpec : FunSpec({ test("addition is commutative") { checkAll(Arb.positiveLong(), Arb.positiveLong()) { a, b -> Money(a) + Money(b) shouldBe Money(b) + Money(a) } } test("cannot create negative money") { shouldThrow<IllegalArgumentException> { Money(-1) } } }) ``` ### assertSoftly + Parameterized Tests ```kotlin @Test fun `user has correct defaults`() { val user = User.create("Alice", "alice@test.com") assertSoftly(user) { name shouldBe "Alice" role shouldBe Role.USER isActive shouldBe true } } @ParameterizedTest @CsvSource("alice@test.com, true", "not-an-email, false", "'', false") fun `validates email format`(input: String, expected: Boolean) { assertThat(isValidEmail(input)).isEqualTo(expected) } ``` --- ## Common Frameworks ### Spring Boot with Kotlin ```kotlin @RestController @RequestMapping("/api/v1/users") class UserController(private val userService: UserService) { @GetMapping("/{id}") fun getUser(@PathVariable id: String): ResponseEntity<UserDto> = userService.findById(UserId(id))?.let { ResponseEntity.ok(it.toDto()) } ?: ResponseEntity.notFound().build() @PostMapping fun createUser(@Valid @RequestBody req: CreateUserRequest): ResponseEntity<UserDto> { val user = userService.create(req) return ResponseEntity.created(URI("/api/v1/users/${user.id.value}")).body(user.toDto()) } } ``` ### Ktor ```kotlin fun Application.configureRouting() { routing { route("/api/v1/users") { get { call.respond(userService.listAll()) } get("/{id}") { val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest) val user = userService.findById(UserId(id)) ?: return@get call.respond(HttpStatusCode.NotFound) call.respond(user) } post { val req = call.receive<CreateUserRequest>() call.respond(HttpStatusCode.Created, userService.create(req)) } } } } fun Application.configurePlugins() { install(ContentNegotiation) { json() } install(StatusPages) { exception<DomainError.NotFound> { call, e -> call.respond(HttpStatusCode.NotFound, e.message) } } } ``` ### Exposed (SQL DSL) ```kotlin object Users : Table("users") { val id = varchar("id", 36) val name = varchar("name", 255) val email = varchar("email", 255).uniqueIndex() val role = enumerationByName<Role>("role", 20) override val primaryKey = PrimaryKey(id) } suspend fun findByRole(role: Role): List<User> = newSuspendedTransaction(Dispatchers.IO) { Users.selectAll().where { Users.role eq role }.map { it.toUser() } } ``` ### kotlinx.serialization ```kotlin @Serializable data class ApiResponse<T>(val data: T, val meta: Meta? = null) @Serializable data class Meta(val page: Int, val totalPages: Int, val totalItems: Long) ``` ### Koin (DI) ```kotlin val appModule = module { singleOf(::UserRepository) singleOf(::UserService) factoryOf(::CreateUserUseCase) } fun Application.configureKoin() { install(Koin) { modules(appModule) } } ``` --- ## Performance Tips ### Inline Functions ```kotlin inline fun <T> measureTimeAndReturn(block: () -> T): Pair<T, Duration> { val start = System.nanoTime() val result = block() return result to Duration.ofNanos(System.nanoTime() - start) } // crossinline: prevents non-local returns inline fun transaction(crossinline block: () -> Unit) { begin(); try { block() } catch (e: Exception) { rollback(); throw e }; commit() } ``` ### Value Classes ```kotlin @JvmInline value class Email(val value: String) { init { require(value.contains("@")) } } @JvmInline value class Meters(val value: Double) // Compiles to raw String/Double -- zero object allocation ``` ### Sequences vs Lists ```kotlin // Bad: 3 intermediate lists users.filter { it.isActive }.map { it.name }.take(10) // Good: lazy, single pass, stops after 10 users.asSequence().filter { it.isActive }.map { it.name }.take(10).toList() // Rule: sequences for 3+ chained ops on 1000+ elements ``` ### Coroutines ```kotlin suspend fun loadDashboard(userId: String): Dashboard = coroutineScope { val profile = async { userService.getProfile(userId) } val orders = async { orderService.getRecent(userId) } val notifs = async { notificationService.getUnread(userId) } Dashboard(profile.await(), orders.await(), notifs.await()) } fun observeOrders(): Flow<Order> =
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기