Communitygithub.com

patrons-kotlin

Patrons idiomatiques Kotlin, meilleures pratiques et conventions pour construire des applications Kotlin robustes, efficaces et maintenables avec coroutines, sécurité null et DSL builders. Utilisez-le lors de l'écriture ou de la révision de code Kotlin et de sa structure idiomatique ou de sa sécurité null.

Qu'est-ce que patrons-kotlin ?

Ce skill fournit un guide complet des patrons idiomatiques Kotlin et des meilleures pratiques pour développer des applications robustes, efficaces et maintenables. Il couvre sept domaines clés : la sécurité null grâce au système de types et aux opérateurs d'appel sécurisé, l'immuabilité via `val` et `copy()` sur les classes de données, les classes et interfaces scellées pour des hiérarchies exhaustives, la concurrence structurée avec coroutines et `Flow`, les fonctions d'extension pour ajouter du comportement sans héritage, les builders DSL type-safe utilisant `@DslMarker` et les récepteurs lambda, et le DSL Kotlin Gradle pour la configuration de build. Le skill propose des exemples concrets et des anti-patrons, aide à éviter les pièges courants, et s'applique à l'écriture, la révision et la refactorisation de code Kotlin, ainsi qu'à la conception de modules et de bibliothèques. Il s'intègre facilement dans les workflows des développeurs utilisant des IDE comme IntelliJ IDEA et des outils de build comme Gradle avec Kotlin DSL.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Patrons de Développement Kotlin

Patrons idiomatiques Kotlin et meilleures pratiques pour construire des applications robustes, efficaces et maintenables.

Quand l'utiliser

  • Écrire du nouveau code Kotlin
  • Réviser du code Kotlin
  • Refactoriser du code Kotlin existant
  • Concevoir des modules ou bibliothèques Kotlin
  • Configurer des builds avec Gradle Kotlin DSL

Comment ça marche

Ce skill applique des conventions idiomatiques Kotlin dans sept domaines clés : la sécurité null via le système de types et les opérateurs d'appel sécurisé, l'immuabilité via val et copy() sur les classes de données, les classes et interfaces scellées pour des hiérarchies exhaustives, la concurrence structurée avec coroutines et Flow, les fonctions d'extension pour ajouter du comportement sans héritage, les builders DSL type-safe utilisant @DslMarker et les récepteurs lambda, et le DSL Kotlin Gradle pour la configuration de build.

Exemples

Sécurité null avec l'opérateur Elvis :

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

Classe scellée pour des résultats exhaustifs :

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

Concurrence structurée avec 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())
    }

Principes Fondamentaux

1. Sécurité Null

Le système de types de Kotlin distingue les types nullable et non-nullable. Exploitez-le pleinement.

// Bon : Utiliser des types non-nullables par défaut
fun getUser(id: String): User {
    return userRepository.findById(id)
        ?: throw UserNotFoundException("Utilisateur $id non trouvé")
}

// Bon : Appels sécurisés et opérateur Elvis
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

// Mauvais : Forcer le déballage de types nullables
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user!!.email // Lève une NPE si null
}

2. Immuabilité par Défaut

Préférez val à var, les collections immuables aux collections mutables.

// Bon : Données immuables
data class User(
    val id: String,
    val name: String,
    val email: String,
)

// Bon : Transformer avec copy()
fun updateEmail(user: User, newEmail: String): User =
    user.copy(email = newEmail)

// Bon : Collections immuables
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }

// Mauvais : État mutable
var currentUser: User? = null // Éviter l'état global mutable
val mutableUsers = mutableListOf<User>() // Éviter sauf si vraiment nécessaire

3. Corps d'Expression et Fonctions Mono-Expression

Utilisez des corps d'expression pour des fonctions concises et lisibles.

// Bon : Corps d'expression
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('@') }

// Bon : When comme expression
fun statusMessage(code: Int): String = when (code) {
    200 -> "OK"
    404 -> "Non trouvé"
    500 -> "Erreur Interne du Serveur"
    else -> "Statut inconnu : $code"
}

// Mauvais : Corps de bloc inutile
fun isAdult(age: Int): Boolean {
    return age >= 18
}

4. Classes de Données pour les Objets Valeur

Utilisez des classes de données pour les types qui contiennent principalement des données.

// Bon : Classe de données avec copy, equals, hashCode, toString
data class CreateUserRequest(
    val name: String,
    val email: String,
    val role: Role = Role.USER,
)

// Bon : Classe valeur pour la sécurité de type (zéro surcharge à l'exécution)
@JvmInline
value class UserId(val value: String) {
    init {
        require(value.isNotBlank()) { "UserId ne peut pas être vide" }
    }
}

@JvmInline
value class Email(val value: String) {
    init {
        require('@' in value) { "Email invalide : $value" }
    }
}

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

Classes et Interfaces Scellées

Modélisation de Hiérarchies Restreintes

// Bon : Classe scellée pour un when exhaustif
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("Encore en chargement")
}

Interfaces Scellées pour les Réponses 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
}

Fonctions de Portée

Quand Utiliser Chacune

// let : Transformer un résultat nullable ou mis en portée
val length: Int? = name?.let { it.trim().length }

// apply : Configurer un objet (retourne l'objet)
val user = User().apply {
    name = "Alice"
    email = "[email protected]"
}

// also : Effets secondaires (retourne l'objet)
val user = createUser(request).also { logger.info("Utilisateur créé : ${it.id}") }

// run : Exécuter un bloc avec récepteur (retourne le résultat)
val result = connection.run {
    prepareStatement(sql)
    executeQuery()
}

// with : Forme non-extension de run
val csv = with(StringBuilder()) {
    appendLine("nom,email")
    users.forEach { appendLine("${it.name},${it.email}") }
    toString()
}

Anti-Patrons

// Mauvais : Imbrication de fonctions de portée
user?.let { u ->
    u.address?.let { addr ->
        addr.city?.let { city ->
            println(city) // Difficile à lire
        }
    }
}

// Bon : Enchaîner les appels sécurisés à la place
val city = user?.address?.city
city?.let { println(it) }

Fonctions d'Extension

Ajouter des Fonctionnalités Sans Héritage

// Bon : Extensions spécifiques au domaine
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()

// Bon : Extensions de collection
fun <T> List<T>.second(): T = this[1]

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

// Bon : Extensions limitées (ne pollue pas l'espace de noms global)
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() }
}

Coroutines

Concurrence Structurée

// Bon : Concurrence structurée avec 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(),
        )
    }

// Bon : supervisorScope quand les enfants peuvent échouer indépendamment
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 pour les Flux Réactifs

// Bon : Flux froid avec gestion d'erreurs appropriée
fun observeUsers(): Flow<List<User>> = flow {
    while (currentCoroutineContext().isActive) {
        val users = userRepository.findAll()
        emit(users)
        delay(5.seconds)
    }
}.catch { e ->
    logger.error("Erreur lors de l'observation des utilisateurs", e)
    emit(emptyList())
}

// Bon : Opérateurs 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()) }

Annulation et Nettoyage

// Bon : Respecter l'annulation
suspend fun processItems(items: List<Item>) {
    items.forEach { item ->
        ensureActive() // Vérifier l'annulation avant un travail coûteux
        processItem(item)
    }
}

// Bon : Nettoyage avec try/finally
suspend fun acquireAndProcess() {
    val resource = acquireResource()
    try {
        resource.process()
    } finally {
        withContext(NonCancellable) {
            resource.release() // Toujours libérer, même en cas d'annulation
        }
    }
}

Délégation

Délégation de Propriété

// Initialisation paresseuse
val expensiveData: List<User> by lazy {
    userRepository.findAll()
}

// Propriété observable
var name: String by Delegates.observable("initial") { _, old, new ->
    logger.info("Nom changé de '$old' à '$new'")
}

// Propriétés basées sur une Map
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))

Délégation d'Interface

// Bon : Déléguer l'implémentation d'interface
class LoggingUserRepository(
    private val delegate: UserRepository,
    private val logger: Logger,
) : UserRepository by delegate {
    // Ne surcharger que ce qui est nécessaire pour ajouter de la journalisation
    override suspend fun findById(id: String): User? {
        logger.info("Recherche d'utilisateur par id : $id")
        return delegate.findById(id).also {
            logger.info("Utilisateur trouvé : ${it?.name ?: "null"}")
        }
    }
}

Builders DSL

Builders Type-Safe

// Bon : DSL avec @DslMarker
@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)

// Utilisation
val page = html {
    head { title("Ma Page") }
    body {
        h1("Bienvenue")
        p("Bonjour le monde !")
    }
}

DSL de Configuration

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

// Utilisation
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)
}

Sequences pour l'Évaluation Paresseuse

// Bon : Utiliser les sequences pour les grandes collections avec de multiples opérations
val result = users.asSequence()
    .filter { it.isActive }
    .map { it.email }
    .filter { it.endsWith("@company.com") }
    .take(10)
    .toList()

// Bon : Générer des sequences infinies
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

Configuration build.gradle.kts

// Vérifiez les dernières versions : https://kotlinlang.org/docs/releases.html
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:1.
...

[truncated for translation]

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.

Skills associés