Claude
Skills
Sign in
Back

kotlin-patterns

Included with Lifetime
$97 forever

Patrones idiomáticos de Kotlin, buenas prácticas y convenciones para construir aplicaciones Kotlin robustas, eficientes y mantenibles con coroutines, null safety y builders de DSL.

General

What this skill does


# Patrones de Desarrollo Kotlin

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

## Cuándo Usar

- Escribir nuevo código Kotlin
- Revisar código Kotlin
- Refactorizar código Kotlin existente
- Diseñar módulos o librerías Kotlin
- Configurar builds con Gradle Kotlin DSL

## Cómo Funciona

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

## Ejemplos

**Null safety con el operador Elvis:**
```kotlin
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}
```

**Sealed class para resultados exhaustivos:**
```kotlin
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:**
```kotlin
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. Null Safety

El sistema de tipos de Kotlin distingue tipos nullable de no-nullable. Aprovecharlo al máximo.

```kotlin
// Bien: Usar tipos no-nullable por defecto
fun getUser(id: String): User {
    return userRepository.findById(id)
        ?: throw UserNotFoundException("User $id not found")
}

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

// Mal: Desempaquetar forzadamente tipos nullable
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user!!.email // Lanza NPE si es null
}
```

### 2. Inmutabilidad por Defecto

Preferir `val` sobre `var`, colecciones inmutables sobre mutables.

```kotlin
// Bien: Datos inmutables
data class User(
    val id: String,
    val name: String,
    val email: String,
)

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

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

// Mal: 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

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

```kotlin
// Bien: 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('@') }

// Bien: When como expresión
fun statusMessage(code: Int): String = when (code) {
    200 -> "OK"
    404 -> "Not Found"
    500 -> "Internal Server Error"
    else -> "Unknown status: $code"
}

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

### 4. Data Classes para Objetos de Valor

Usar data classes para tipos que principalmente contienen datos.

```kotlin
// Bien: Data class con copy, equals, hashCode, toString
data class CreateUserRequest(
    val name: String,
    val email: String,
    val role: Role = Role.USER,
)

// Bien: Value class para type safety (cero overhead en tiempo de ejecución)
@JvmInline
value class UserId(val value: String) {
    init {
        require(value.isNotBlank()) { "UserId cannot be blank" }
    }
}

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

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

## Clases Selladas e Interfaces

### Modelar Jerarquías Restringidas

```kotlin
// Bien: Sealed class 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("Still loading")
}
```

### Sealed Interfaces para Respuestas de API

```kotlin
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 Scope

### Cuándo Usar Cada Una

```kotlin
// let: Transformar resultado nullable o delimitado
val length: Int? = name?.let { it.trim().length }

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

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

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

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

### Anti-Patrones

```kotlin
// Mal: Anidar funciones de scope
user?.let { u ->
    u.address?.let { addr ->
        addr.city?.let { city ->
            println(city) // Difícil de leer
        }
    }
}

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

## Funciones de Extensión

### Agregar Funcionalidad Sin Herencia

```kotlin
// Bien: 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()

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

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

// Bien: Extensiones delimitadas (sin contaminar el namespace 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

### Concurrencia Estructurada

```kotlin
// Bien: 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(),
            pos

Related in General