Communitygithub.com

kotlin-patterns

慣用的なKotlinパターン、ベストプラクティス、およびコルーチン、Null安全、DSLビルダーを使用した堅牢で効率的かつ保守可能なKotlinアプリケーションの構築に関する規約です。Kotlinコードの作成やレビュー、慣用的な構造やNull安全性が問題になる場合に使用します。

kotlin-patterns とは?

このスキルは、Kotlin開発者が慣用的なパターンとベストプラクティスに従って、堅牢で効率的かつ保守可能なアプリケーションを構築するための包括的なガイドです。Null安全、不変性、シールクラス、コルーチン、拡張関数、DSLビルダー、Gradle Kotlin DSLといった7つの主要領域をカバーしています。具体的なコード例やアンチパターンを示すことで、開発者がKotlinの型システムを最大限に活用し、安全で表現力豊かなコードを記述できるよう支援します。このスキルは、Claude CodeやCoursor、CodexなどのAIエージェントと連携し、コード生成やレビュー時に自動的に適用されます。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/kotlin-patterns

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Kotlin開発パターン

堅牢で効率的かつ保守可能なアプリケーションを構築するための、慣用的なKotlinパターンとベストプラクティスです。

使用するタイミング

  • 新しいKotlinコードを作成するとき
  • Kotlinコードをレビューするとき
  • 既存のKotlinコードをリファクタリングするとき
  • Kotlinモジュールやライブラリを設計するとき
  • Gradle Kotlin DSLビルドを設定するとき

仕組み

このスキルは、7つの主要領域にわたって慣用的なKotlinの規約を適用します。型システムと安全呼び出し演算子を使用したNull安全、valとデータクラスのcopy()による不変性、網羅的な型階層のためのシールドクラスとインターフェース、コルーチンとFlowを使用した構造化された並行処理、継承なしで動作を追加する拡張関数、@DslMarkerとラムダレシーバーを使用した型安全なDSLビルダー、そしてビルド設定のためのGradle Kotlin DSLです。

例

Elvis演算子を使ったNull安全:

fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

網羅的な結果のためのシールドクラス:

sealed 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>()
}

async/awaitを使った構造化された並行処理:

suspend 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())
    }

基本原則

1. Null安全

Kotlinの型システムは、Null許容型と非Null型を区別します。それを最大限に活用しましょう。

// 良い例: デフォルトで非Null型を使用する
fun getUser(id: String): User {
    return userRepository.findById(id)
        ?: throw UserNotFoundException("User $id not found")
}

// 良い例: 安全呼び出しとElvis演算子
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

// 悪い例: Null許容型の強制アンラップ
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user!!.email // nullの場合NPEをスロー
}

2. デフォルトで不変

varよりもvalを、可変コレクションよりも不変コレクションを優先しましょう。

// 良い例: 不変データ
    data class User(
    val id: String,
    val name: String,
    val email: String,
)

// 良い例: copy()で変換
fun updateEmail(user: User, newEmail: String): User =
    user.copy(email = newEmail)

// 良い例: 不変コレクション
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }

// 悪い例: 可変状態
var currentUser: User? = null // 可変なグローバル状態は避ける
val mutableUsers = mutableListOf<User>() // 本当に必要な場合を除き避ける

3. 式本体と単一式関数

簡潔で読みやすい関数には式本体を使用します。

// 良い例: 式本体
fun isAdult(age: Int): Boolean = age >= 18

fun formatFullName(first: String, last: String): String =
    "$first $last".trim()

fun User.displayName(): String =
    name.ifBlank { email.substringBefore('@') }

// 良い例: 式としてのwhen
fun statusMessage(code: Int): String = when (code) {
    200 -> "OK"
    404 -> "Not Found"
    500 -> "Internal Server Error"
    else -> "Unknown status: $code"
}

// 悪い例: 不必要なブロック本体
fun isAdult(age: Int): Boolean {
    return age >= 18
}

4. 値オブジェクトのためのデータクラス

主にデータを保持する型にはデータクラスを使用します。

// 良い例: copy、equals、hashCode、toStringを持つデータクラス
data class CreateUserRequest(
    val name: String,
    val email: String,
    val role: Role = Role.USER,
)

// 良い例: 型安全のための値クラス(実行時のオーバーヘッドなし)
@JvmInline
value class UserId(val value: String) {
    init {
        require(value.isNotBlank()) { "UserIdを空にできません" }
    }
}

@JvmInline
value class Email(val value: String) {
    init {
        require('@' in value) { "無効なメールアドレス: $value" }
    }
}

fun getUser(id: UserId): User = userRepository.findById(id)

シールドクラスとインターフェース

制限付き階層のモデリング

// 良い例: 網羅的なwhenのためのシールドクラス
sealed 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")
}

APIレスポンスのためのシールドインターフェース

sealed 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
}

スコープ関数

それぞれを使用するタイミング

// let: Null許容またはスコープ化された結果を変換
val length: Int? = name?.let { it.trim().length }

// apply: オブジェクトを設定(オブジェクトを返す)
val user = User().apply {
    name = "Alice"
    email = "[email protected]"
}

// also: 副作用(オブジェクトを返す)
val user = createUser(request).also { logger.info("Created user: ${it.id}") }

// run: レシーバーでブロックを実行(結果を返す)
val result = connection.run {
    prepareStatement(sql)
    executeQuery()
}

// with: runの非拡張形式
val csv = with(StringBuilder()) {
    appendLine("name,email")
    users.forEach { appendLine("${it.name},${it.email}") }
    toString()
}

アンチパターン

// 悪い例: スコープ関数のネスト
user?.let { u ->
    u.address?.let { addr ->
        addr.city?.let { city ->
            println(city) // 読みにくい
        }
    }
}

// 良い例: 代わりに安全呼び出しをチェーン
val city = user?.address?.city
city?.let { println(it) }

拡張関数

継承なしで機能を追加

// 良い例: ドメイン固有の拡張
fun 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()

// 良い例: コレクション拡張
fun <T> List<T>.second(): T = this[1]

fun <T> List<T>.secondOrNull(): T? = getOrNull(1)

// 良い例: スコープ化された拡張(グローバル名前空間を汚染しない)
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() }
}

コルーチン

構造化された並行処理

// 良い例: coroutineScopeを使った構造化された並行処理
suspend 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(),
        )
    }

// 良い例: 子が独立して失敗する可能性がある場合はsupervisorScope
suspend 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

// 良い例: 適切なエラーハンドリングを伴うコールドフロー
fun 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())
}

// 良い例: Flow演算子
fun searchUsers(query: Flow<String>): Flow<List<User>> =
    query
        .debounce(300.milliseconds)
        .distinctUntilChanged()
        .filter { it.length >= 2 }
        .mapLatest { q -> userRepository.search(q) }
        .catch { emit(emptyList()) }

キャンセルとクリーンアップ

// 良い例: キャンセルを尊重する
suspend fun processItems(items: List<Item>) {
    items.forEach { item ->
        ensureActive() // 高負荷な処理の前にキャンセルをチェック
        processItem(item)
    }
}

// 良い例: try/finallyでのクリーンアップ
suspend fun acquireAndProcess() {
    val resource = acquireResource()
    try {
        resource.process()
    } finally {
        withContext(NonCancellable) {
            resource.release() // キャンセル時でも常に解放
        }
    }
}

委譲

プロパティ委譲

// 遅延初期化
val expensiveData: List<User> by lazy {
    userRepository.findAll()
}

// 監視可能なプロパティ
var name: String by Delegates.observable("initial") { _, old, new ->
    logger.info("Name changed from '$old' to '$new'")
}

// マップをバックエンドとしたプロパティ
class 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))

インターフェース委譲

// 良い例: インターフェース実装の委譲
class LoggingUserRepository(
    private val delegate: UserRepository,
    private val logger: Logger,
) : UserRepository by delegate {
    // ログを追加する必要があるものだけをオーバーライド
    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ビルダー

型安全なビルダー

// 良い例: @DslMarkerを使用したDSL
@DslMarker
annotation class HtmlDsl

@HtmlDsl
class 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)

// 使用例
val page = html {
    head { title("My Page") }
    body {
        h1("Welcome")
        p("Hello, World!")
    }
}

設定DSL

data 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()

// 使用例
val config = serverConfig {
    host = "0.0.0.0"
    port = 443
    ssl("/certs/cert.pem", "/certs/key.pem")
    database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20)
}

遅延評価のためのシーケンス

// 良い例: 複数の操作がある大規模コレクションにはシーケンスを使用
val result = users.asSequence()
    .filter { it.isActive }
    .map { it.email }
    .filter { it.endsWith("@company.com") }
    .take(10)
    .toList()

// 良い例: 無限シーケンスの生成
val 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の設定

plugins {
    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:0.41.1")
    implementation("org.jetbrains.exposed:exposed-dao:0.41.1")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")

    // Logging
    implementation("ch.qos.logback:logback-classic:1.4.12")

    // Testing
    testImplementation(kotlin("test"))
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

リソース

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

関連スキル