Communitygithub.com

kotlin-patterns

Patrones idiomáticos de Kotlin, mejores prácticas y convenciones para construir aplicaciones robustas, eficientes y mantenibles con corrutinas, seguridad de nulos y constructores DSL. Úselo al escribir o revisar código Kotlin y cuando se cuestione la estructura idiomática o la seguridad de nulos.

¿Qué es kotlin-patterns?

Este skill proporciona un conjunto completo de patrones y mejores prácticas para el desarrollo en Kotlin, cubriendo desde seguridad de nulos e inmutabilidad hasta corrutinas, extensiones, sellado de clases y DSL. Está diseñado para ayudar a desarrolladores a escribir código más limpio, robusto y eficiente, siguiendo las convenciones más aceptadas por la comunidad. Soporta cualquier plataforma que utilice Kotlin (JVM, Android, Kotlin Multiplatform) y se integra perfectamente con IDEs como IntelliJ IDEA y Android Studio, así como con herramientas de línea de comandos. El skill se basa en un enfoque práctico, presentando ejemplos de código concretos que muestran tanto el 'buen' uso como los anti-patrones a evitar, lo que facilita su aplicación inmediata en proyectos reales.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Patrones de Desarrollo en Kotlin

Patrones idiomáticos de Kotlin y mejores prácticas para construir aplicaciones robustas, eficientes y mantenibles.

Cuándo Usarlo

  • Escribir nuevo código Kotlin
  • Revisar código Kotlin
  • Refactorizar código Kotlin existente
  • Diseñar módulos o bibliotecas en Kotlin
  • Configurar compilaciones de Gradle con Kotlin DSL

Cómo Funciona

Este skill aplica convenciones idiomáticas de Kotlin en siete áreas clave: seguridad de nulos usando el sistema de tipos y operadores de llamada segura, inmutabilidad mediante val y copy() en clases de datos, clases e interfaces selladas para jerarquías de tipos exhaustivas, concurrencia estructurada con corrutinas y Flow, funciones de extensión para agregar comportamiento sin herencia, constructores DSL type-safe usando @DslMarker y receptores lambda, y Gradle Kotlin DSL para la configuración de compilación.

Ejemplos

Seguridad de nulos con operador Elvis:

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

Clase sellada para resultados exhaustivos:

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

Concurrencia estructurada con 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())
    }

Principios Fundamentales

1. Seguridad de Nulos

El sistema de tipos de Kotlin distingue tipos nullable y no nullable. Aprovéchelo completamente.

// Bueno: Usar tipos no nullable por defecto
fun getUser(id: String): User {
    return userRepository.findById(id)
        ?: throw UserNotFoundException("Usuario $id no encontrado")
}

// Bueno: Llamadas seguras y operador Elvis
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

// Malo: Forzar desenrollado de tipos nullable
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user!!.email // Lanza NPE si es null
}

2. Inmutabilidad por Defecto

Prefiera val sobre var, colecciones inmutables sobre mutables.

// Bueno: Datos inmutables
data class User(
    val id: String,
    val name: String,
    val email: String,
)

// Bueno: Transformar con copy()
fun updateEmail(user: User, newEmail: String): User =
    user.copy(email = newEmail)

// Bueno: Colecciones inmutables
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }

// Malo: Estado mutable
var currentUser: User? = null // Evitar estado global mutable
val mutableUsers = mutableListOf<User>() // Evitar a menos que sea realmente necesario

3. Cuerpos de Expresión y Funciones de una Sola Expresión

Use cuerpos de expresión para funciones concisas y legibles.

// Bueno: Cuerpo de expresión
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('@') }

// Bueno: When como expresión
fun statusMessage(code: Int): String = when (code) {
    200 -> "OK"
    404 -> "No Encontrado"
    500 -> "Error Interno del Servidor"
    else -> "Estado desconocido: $code"
}

// Malo: Cuerpo de bloque innecesario
fun isAdult(age: Int): Boolean {
    return age >= 18
}

4. Clases de Datos para Objetos de Valor

Use clases de datos para tipos que contienen principalmente datos.

// Bueno: Clase de datos con copy, equals, hashCode, toString
data class CreateUserRequest(
    val name: String,
    val email: String,
    val role: Role = Role.USER,
)

// Bueno: Clase de valor para seguridad de tipos (sin overhead en tiempo de ejecución)
@JvmInline
value class UserId(val value: String) {
    init {
        require(value.isNotBlank()) { "UserId no puede estar vacío" }
    }
}

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

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

Clases e Interfaces Selladas

Modelado de Jerarquías Restringidas

// Bueno: Clase sellada para when exhaustivo
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("Todavía cargando")
}

Interfaces Selladas para Respuestas de 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
}

Funciones de Ámbito

Cuándo Usar Cada Una

// let: Transformar resultado nullable o con ámbito
val length: Int? = name?.let { it.trim().length }

// apply: Configurar un objeto (devuelve el objeto)
val user = User().apply {
    name = "Alice"
    email = "[email protected]"
}

// also: Efectos secundarios (devuelve el objeto)
val user = createUser(request).also { logger.info("Usuario creado: ${it.id}") }

// run: Ejecutar un bloque con receptor (devuelve resultado)
val result = connection.run {
    prepareStatement(sql)
    executeQuery()
}

// with: Forma no extensión de run
val csv = with(StringBuilder()) {
    appendLine("nombre,email")
    users.forEach { appendLine("${it.name},${it.email}") }
    toString()
}

Anti-patrones

// Malo: Anidar funciones de ámbito
user?.let { u ->
    u.address?.let { addr ->
        addr.city?.let { city ->
            println(city) // Difícil de leer
        }
    }
}

// Bueno: Encadenar llamadas seguras en su lugar
val city = user?.address?.city
city?.let { println(it) }

Funciones de Extensión

Agregar Funcionalidad Sin Herencia

// Bueno: Extensiones específicas del dominio
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()

// Bueno: Extensiones de colecciones
fun <T> List<T>.second(): T = this[1]

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

// Bueno: Extensiones con ámbito (no contaminan el espacio de nombres 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() }
}

Corrutinas

Concurrencia Estructurada

// Bueno: Concurrencia estructurada con 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(),
        )
    }

// Bueno: supervisorScope cuando los hijos pueden fallar independientemente
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 para Flujos Reactivos

// Bueno: Flow frío con manejo adecuado de errores
fun observeUsers(): Flow<List<User>> = flow {
    while (currentCoroutineContext().isActive) {
        val users = userRepository.findAll()
        emit(users)
        delay(5.seconds)
    }
}.catch { e ->
    logger.error("Error al observar usuarios", e)
    emit(emptyList())
}

// Bueno: Operadores de 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()) }

Cancelación y Limpieza

// Bueno: Respetar la cancelación
suspend fun processItems(items: List<Item>) {
    items.forEach { item ->
        ensureActive() // Verificar cancelación antes de trabajo costoso
        processItem(item)
    }
}

// Bueno: Limpieza con try/finally
suspend fun acquireAndProcess() {
    val resource = acquireResource()
    try {
        resource.process()
    } finally {
        withContext(NonCancellable) {
            resource.release() // Siempre liberar, incluso en cancelación
        }
    }
}

Delegación

Delegación de Propiedades

// Inicialización perezosa
val expensiveData: List<User> by lazy {
    userRepository.findAll()
}

// Propiedad observable
var name: String by Delegates.observable("inicial") { _, old, new ->
    logger.info("Nombre cambiado de '$old' a '$new'")
}

// Propiedades respaldadas por mapa
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))

Delegación de Interfaces

// Bueno: Delegar la implementación de una interfaz
class LoggingUserRepository(
    private val delegate: UserRepository,
    private val logger: Logger,
) : UserRepository by delegate {
    // Solo sobrescribir lo necesario para agregar registro
    override suspend fun findById(id: String): User? {
        logger.info("Buscando usuario por id: $id")
        return delegate.findById(id).also {
            logger.info("Usuario encontrado: ${it?.name ?: "null"}")
        }
    }
}

Constructores DSL

Constructores Type-Safe

// Bueno: DSL con @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)

// Uso
val page = html {
    head { title("Mi Página") }
    body {
        h1("Bienvenido")
        p("¡Hola, Mundo!")
    }
}

DSL de Configuración

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

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

Secuencias para Evaluación Perezosa

// Bueno: Usar secuencias para colecciones grandes con múltiples operaciones
val result = users.asSequence()
    .filter { it.isActive }
    .map { it.email }
    .filter { it.endsWith("@company.com") }
    .take(10)
    .toList()

// Bueno: Generar secuencias infinitas
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

Configuración de build.gradle.kts

// Verifique las últimas versiones: 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.")

…

[truncat para traducción]

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 relacionados