Guía de Kotlin: De 0 a 100

Guía de Kotlin: De 0 a 100

Desde los fundamentos hasta producción: null safety, corrutinas, multiplataforma, frameworks, proyectos reales y recursos en español.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:58 MINUTOS DE LECTURA

Guía de Kotlin: De 0 a 100

¿Qué es Kotlin?

Kotlin es un lenguaje de programación moderno, de tipado estático y multiparadigma, creado por JetBrains (los mismos que hacen IntelliJ IDEA) y lanzado en 2011. Su primera versión estable (1.0) llegó en febrero de 2016. En 2017 Google anunció soporte oficial para Android, y en 2019 Android pasó a ser Kotlin-first.

Historia breve:

  • 2011: JetBrains presenta Kotlin como proyecto open source
  • 2016: Kotlin 1.0 — primera versión estable
  • 2017: Google anuncia soporte oficial para Android en Google I/O
  • 2018: Kotlin 1.3 — corrutinas estables, multiplataforma experimental
  • 2019: Android se declara Kotlin-first
  • 2020: Kotlin 1.4 — multiplataforma estable, mejoras en el compilador
  • 2021: Kotlin 1.5 — sealed interfaces, nuevos tipos JVM
  • 2023: Kotlin 1.9 — K2 compiler (nuevo compilador), mejoras en KMP
  • 2024+: Kotlin 2.0 — K2 compiler estable, Compose Multiplatform estable

¿Dónde se usa?

  • Android: El lenguaje oficial para desarrollo Android. Jetpack Compose, Android KTX, todo el ecosistema moderno está en Kotlin
  • Backend / Web: Ktor (async, ligero), Spring Boot con Kotlin, http4k, Jooby
  • Multiplataforma (KMP): Comparte lógica de negocio entre Android, iOS, web, desktop con Kotlin Multiplatform
  • Native: Kotlin/Native compila a binarios sin JVM (iOS, macOS, Linux, Windows, WebAssembly)
  • JavaScript/Web: Kotlin/JS compila a JavaScript, compatible con React, Node.js
  • Data Science: Kotlin for Data Science (KotlinDL, kmath, lets-plot), compatible con Python via GraalPy
  • Serverless / Cloud: AWS SDK for Kotlin, funciones Lambda en Kotlin, Ktor en GraalVM Native Image

¿Quién lo usa? Google (aplicaciones Android, Firebase), Netflix (orquestación interna), Twitter (Performance team), Pinterest (Android), Trello (Android), Uber, Square, Gradle (build tool escrita en Kotlin).

Fuente: Kotlin.org — About, Google Android Docs — Kotlin, Wikipedia — Kotlin


Prerrequisitos

Antes de empezar, necesitas:

  • Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
  • El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)

Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.

¿Cómo empezar? Instalación en todos los entornos

SDKMAN (Linux/macOS/WSL) — recomendado

SDKMAN es el gestor de versiones de JDK más usado. Instala, cambia y gestiona múltiples JDKs y Kotlin al instante, similar a pyenv o nvm.

curl -s https://get.sdkman.io | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

sdk list java
sdk install java 21.0.2-temurin
sdk install kotlin
sdk default java 21.0.2-temurin

kotlin -version

IntelliJ IDEA

  • Community Edition (gratis): incluye soporte Kotlin nativo. File → New → Project → Kotlin
  • Plug-in Kotlin ya viene incluido desde la versión 15
  • Ultimate Edition: incluye Spring Boot, Ktor, Android, y más frameworks

Android Studio

Descarga desde developer.android.com/studio. Kotlin incluido por defecto. File → New → New Project → Empty Compose Activity (templates con Kotlin).

VS Code

  1. Instala la extensión Kotlin (de Microsoft, language server vía Kotlin LS)
  2. Extensiones adicionales: Kotlin Test Runner, Gradle for Java, Kotlin Multiplatform
  3. npm install -g kotlin-language-server (para completado y navegación)

kotlinc (compilador CLI)

# Manual (sin SDKMAN)
curl -s https://get.sdkman.io | bash && sdk install kotlin

# O descarga el zip desde GitHub releases
wget https://github.com/JetBrains/kotlin/releases/download/v2.1.0/kotlin-compiler-2.1.0.zip
unzip kotlin-compiler-*.zip
export PATH=$PATH:$(pwd)/kotlinc/bin

Gradle (build principal de Kotlin)

# Instalar SDKMAN
sdk install gradle

# O usar el wrapper (recomendado en proyectos)
gradle wrapper --gradle-version 8.5

build.gradle.kts mínimo (Kotlin DSL en Gradle):

plugins {
    kotlin("jvm") version "2.1.0"
    application
}

application {
    mainClass = "MainKt"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib"))
    testImplementation(kotlin("test"))
}

Docker

FROM eclipse-temurin:21-jre
RUN apt-get update && apt-get install -y curl unzip \
    && curl -s https://get.sdkman.io | bash \
    && bash -c "source /root/.sdkman/bin/sdkman-init.sh && sdk install kotlin"
COPY . /app
WORKDIR /app
CMD ["kotlin", "Main.kt"]

Alias útiles para el día a día

Añade a tu ~/.bashrc o ~/.zshrc:

alias k='kotlin'
alias kotlin-repl='kotlin -version 2>/dev/null || sdk use kotlin; kotlin'
alias kg='kotlinc-jvm'
alias kw='gradle wrapper --gradle-version'
alias ktest='gradle test'
alias kbuild='gradle build'
alias krun='gradle run'

Fuente: SDKMAN docs, Kotlin Compiler manual, Docker Hub — openjdk


Escala de aprendizaje: de 0 a 100

Nivel 0–15: Fundamentos absolutos

Qué aprender:

  • Sintaxis básica: fun, val/var, tipos (Int, Double, String, Boolean)
  • println(), templates strings "Hola $nombre"
  • Condicionales: if/else como expresión, when
  • Bucles: for en rangos, while, forEach
  • Colecciones básicas: listOf(), mapOf(), mutableListOf()
  • Funciones: fun, parámetros con default, expresión body
  • Null safety: String?, ?., ?:, !!
  • Conceptos clave: val vs var, null safety, Int es objeto pero optimiza a primitivo

Proyecto: Adivina el número — programa que elige un número aleatorio entre 1 y 100, readln() para entrada, pistas “mayor/menor”, contador de intentos.

Nivel 15–30: Funciones, colecciones y POO básica

Qué aprender:

  • Lambdas: { x: Int -> x * 2 }, it implícito
  • Funciones de orden superior: map, filter, reduce, fold
  • Sequence vs List: lazy vs eager
  • Clases: constructores, init, this
  • Data classes: copy(), destructuring
  • Propiedades con getters/setters, field
  • Extension functions, companion object, object
  • Algoritmos: sorted, sortedBy, groupingBy, fold/reduce, partition
  • Big O: tabla de colecciones

Proyecto: Gestor de tareas CLI — añadir, listar, completar, eliminar con data class Tarea, object GestorTareas, persistencia JSON con kotlinx.serialization.

Nivel 30–45: POO avanzada, genéricos y selladas

Qué aprender:

  • Interfaces con implementación por defecto
  • Sealed classes e interfaces, when exhaustivo
  • Genéricos: <T>, out/in, reified, where
  • Delegados: by lazy, by Delegates.observable, personalizados
  • Scope functions: let, run, with, apply, also
  • Operator overloading, inline/noinline/crossinline

Proyecto: Sistema de inventario — data class Producto, sealed class OperacionInventario, class Inventario<T>. Delegado vetoable para evitar stock negativo.

Nivel 45–60: Testing, corrutinas básicas y serialización

Qué aprender:

  • Testing: kotlin.test (JUnit 5)
  • Corrutinas: runBlocking, launch, async/await, delay()
  • suspend functions, Dispatchers.Default vs IO
  • kotlinx.serialization: @Serializable, JSON
  • kotlinx.coroutines.test: runTest, TestScope
  • Manejo de archivos: java.io.File, Path, kotlinx.serialization

Proyecto: Gestor de contraseñas — persistencia cifrada con kotlinx.serialization, corrutinas para cifrado/descifrado, tests parametrizados.

Nivel 60–75: Ktor, Exposed y aplicaciones web

Qué aprender:

  • Ktor: routing, content negotiation, serialization, plugins
  • Exposed: DSL de tablas, queries, transactions
  • Inyección de dependencias: Koin
  • Autenticación: JWT, sesiones
  • Testing de endpoints: testApplication de Ktor

Proyecto: API de recetas — Ktor + Exposed + H2/PostgreSQL, CRUD con autenticación JWT, tests con testApplication + BD en memoria.

Nivel 75–90: Multiplataforma (KMP), Compose y producción

Qué aprender:

  • KMP: estructura de módulos common, android, ios
  • expect/actual declarations
  • Jetpack Compose: @Composable, State, remember, LaunchedEffect
  • Compose Multiplatform: UI compartida
  • CI/CD: GitHub Actions con Gradle caching
  • Docker: Dockerfile multi-etapa para Ktor

Proyecto: App de notas multiplataforma — módulo shared con modelos y lógica (KMP), UI con Compose Multiplatform para Android y Desktop.

Nivel 90–100: Arquitectura, rendimiento y sistemas distribuidos

Qué aprender:

  • Arquitectura limpia: capas domain, data, presentation
  • Event-driven: Kafka con corrutinas, RabbitMQ
  • GraphQL con graphql-kotlin, gRPC con grpc-kotlin
  • Observabilidad: OpenTelemetry, Prometheus
  • Rendimiento: profiling, JMH benchmarking
  • GraalVM Native Image: binario nativo (&lt;100ms startup)
  • Arrow.kt: Either, Option, IO funcional

Proyecto: Sistema de notificaciones distribuidas — microservicios con Ktor y gRPC, Kafka como event bus, GraalVM Native Image, Docker Compose + Prometheus + Grafana.

Fuente: Ktor docs, Exposed docs, Kotlin Multiplatform docs


Primeros pasos y configuración del entorno

Si prefieres ver directamente el “Hola Mundo” en Kotlin, consulta El Atlas del Hola Mundo.

Editores recomendados

Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.

Kotlin REPL

Ejecuta kotlin en terminal (sin archivos) y escribe directamente:

println("Hola Mundo")
2 + 2
val lista = listOf(1, 2, 3)
lista.map { it * 2 }

Para salir: :quit o Ctrl+D.

Kotlin Playground

Navega a play.kotlinlang.org — editor online oficial de JetBrains. Soporta:

  • Programas completos con fun main()
  • Corrutinas, multiplataforma, Java interop
  • Compartir snippets por URL
  • Descargar como archivo .kt

Scripts Kotlin (.kts)

Kotlin también puede ejecutarse como script (sin compilar):

// hola.kts
fun main() {
    println("Hola desde Kotlin script")
}
main()

Ejecuta: kotlin hola.kts

main() sin clase

Kotlin permite funciones de nivel superior. No necesitas clases:

// Main.kt
fun main() {
    println("Hola, mundo!")
}

Compila y ejecuta:

kotlinc-jvm Main.kt -include-runtime -d main.jar
java -jar main.jar
# O usando kotlin directamente:
kotlin Main.kt

Estructura de proyecto con Gradle Kotlin DSL

mi-proyecto/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── src/
│   ├── main/
│   │   └── kotlin/
│   │       └── Main.kt
│   └── test/
│       └── kotlin/
│           └── MainTest.kt
└── gradle/
    └── wrapper/

Tipos y construcción básica

val entero: Int = 42
val largo: Long = 42L
val doble: Double = 3.14
val texto: String = "Hola"
val booleano: Boolean = true
val caracter: Char = 'A'
val bytes: Byte = 127
val array: IntArray = intArrayOf(1, 2, 3)

// Template strings
val nombre = "Mundo"
println("Hola $nombre!")
println("2 + 2 = ${2 + 2}")

// Rangos
for (i in 1..10) print(i)
for (i in 10 downTo 1 step 2) print(i)
for (i in 1 until 10) print(i)

// When (switch potente)
val x = 3
val resultado = when (x) {
    1 -> "uno"
    2 -> "dos"
    in 3..10 -> "varios"
    !in 0..100 -> "fuera de rango"
    else -> "otro"
}

Fuente: Kotlin Playground, Kotlin docs — Basic Syntax


Paradigmas de programación

Kotlin es un lenguaje multiparadigma que combina lo mejor de la programación orientada a objetos y la funcional. No te obliga a elegir uno — puedes mezclarlos libremente.

Programación Orientada a Objetos (POO)

En Kotlin todo es un objeto: no hay tipos primitivos independientes. Incluso los números (Int, Long) son objetos con métodos y propiedades.

val numero = 42
println(numero.toString())       // "42"
println(numero.rangeTo(50))      // 42..50
println(42.compareTo(10))        // 1 (mayor que)
println((-1).coerceAtLeast(0))   // 0

Principios soportados:

  • Clases y objetos: class, object, data class, sealed class
  • Herencia: Jerarquía única con open class y abstract class
  • Interfaces: Con propiedades abstractas y métodos con implementación por defecto
  • Polimorfismo: Por subtipo, paramétrico (genéricos) y ad-hoc (extension functions)
  • Encapsulación: Visibilidad public, private, protected, internal

Programación Funcional

Kotlin incorpora elementos funcionales sin ser puramente funcional. Las funciones son ciudadanos de primera clase.

Lambdas y higher-order functions:

val numeros = listOf(1, 2, 3, 4, 5)

val pares = numeros.filter { it % 2 == 0 }
val cuadrados = numeros.map { it * it }
val suma = numeros.reduce { acc, i -> acc + i }

fun operar(a: Int, b: Int, op: (Int, Int) -> Int): Int = op(a, b)
println(operar(5, 3) { x, y -> x + y })  // 8
println(operar(5, 3) { x, y -> x * y })  // 15

Data classes (value objects funcionales):

data class Persona(val nombre: String, val edad: Int)

Pattern matching con when y sealed classes:

sealed class Tree
data class Leaf(val value: Int) : Tree()
data class Node(val left: Tree, val right: Tree) : Tree()

fun sum(tree: Tree): Int = when (tree) {
    is Leaf -> tree.value
    is Node -> sum(tree.left) + sum(tree.right)
}

Programación orientada a nulabilidad

Kotlin integra la nulabilidad en el sistema de tipos, haciendo que los errores de null pointer sean detectables en compilación.

var nullable: String? = null
var noNullable: String = "Hola"
val longitud = nullable?.length ?: 0

Programación reactiva

Kotlin soporta programación reactiva mediante corrutinas y flujos asíncronos. Flow, StateFlow y SharedFlow son la base del modelo reactivo en Kotlin.

val flujo = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}

class ViewModel {
    private val _state = MutableStateFlow("inicial")
    val state: StateFlow<String> = _state.asStateFlow()
}

Tabla de paradigmas

ParadigmaSoporte nativoLibrerías/APIsEjemplo
POOClases, interfaces, herencia, data classkotlin stdlibclass, interface, data class
FuncionalLambdas, HOF, inmutabilidadkotlin stdlibmap, filter, reduce
Null SafetyT?, ?., ?:, !!Lenguaje nativoString?, elvis operator
ReactivoFlow, StateFlow, SharedFlowkotlinx.coroutinesflow { ... }
DSLLambdas con receiver, type-safe buildersVariosKtor routing, Compose UI

Paradigma principal: Kotlin es principalmente POO + funcional. No es Haskell (puramente funcional) ni Java (puramente POO con lambdas añadidas).

Fuente: Kotlin docs — Idioms, Kotlin docs — Functional Programming, Kotlin Multiplatform docs


Tipos de datos y variables

Sistema de tipos

¿Qué es?

Kotlin tiene tipado estático y fuerte: el tipo de cada variable se conoce en tiempo de compilación y no se realizan conversiones implícitas entre tipos incompatibles. El sistema de tipos es nominal con soporte de inferencia de tipos. A diferencia de Java, Kotlin distingue en el sistema de tipos entre referencias que pueden ser nulas (T?) y las que no (T).

🔗 Wikipedia — Tipado estático, Kotlin docs — Basic Types

Sintaxis básica

val x: Int = 42
val y = "hola"
var z = 3.14

var nullable: String? = null
var noNulo: String = "Hola"

fun saludo(nombre: String): String = "Hola $nombre"

🧪 Cómo probarlo

import kotlin.test.*

class TiposTest {
    @Test
    fun tipadoEstatico() {
        val x: Int = 42
        assertEquals(42, x)
    }

    @Test
    fun inferencia() {
        val texto = "hola"
        assertIs<String>(texto)
    }

    @Test
    fun nullable() {
        var nulo: String? = null
        assertNull(nulo)
        nulo = "hola"
        assertNotNull(nulo)
    }
}

💡 Memoria y rendimiento

  • Los tipos no-nulables (Int, Long) se compilan a primitivos JVM (int, long) cuando no hay nullable o genéricos
  • Los tipos nulables (Int?, Long?) siempre son objetos en el heap (boxing automático)
  • IntArray se almacena como int[] contiguo en heap — más eficiente que List<Int>
  • La inferencia de tipos se resuelve en compilación, sin impacto en runtime
TipoBits (stack)Boxing nullableRango
Int32Int? → objeto heap-2³¹ a 2³¹-1
Long64Long? → objeto heap-2⁶³ a 2⁶³-1
Float32Float? → objeto heap±1.4E-45 a ±3.4E+38
Double64Double? → objeto heap±4.9E-324 a ±1.8E+308

✅ Buenas prácticas

  • ✅ Usa val por defecto, var solo cuando sea estrictamente necesario
  • ✅ Prefiere tipos no-nulables (Int en vez de Int?) para valores que nunca serán null
  • ✅ Usa IntArray en vez de List<Int> para colecciones grandes de primitivos
  • ⚠️ No confundas == (estructural, equals) con === (referencia, identidad)
  • ❌ No uses !! a menos que estés 100% seguro de que el valor no es null

🏗️ Metodología

  • En proyectos nuevos, activa explicitApi = true en Gradle para forzar visibilidad explícita
  • Usa type inference para código más limpio, pero declara tipos explícitamente en APIs públicas
  • En Kotlin Multiplatform, los tipos se comportan igual en todas las plataformas

🔗 Para saber más

Tipos básicos

¿Qué es?

Los tipos básicos son los bloques fundamentales de datos. En Kotlin, a diferencia de Java, todo es un objeto — incluso los números tienen métodos. Sin embargo, el compilador optimiza a primitivos JVM cuando no hay nullable de por medio.

🔗 Kotlin docs — Numbers

Sintaxis básica

TipoTamañoEjemploNotas
Byte8 bits127-128 a 127
Short16 bits32000-32768 a 32767
Int32 bits42-2³¹ a 2³¹-1
Long64 bits42LSufijo L
Float32 bits3.14fSufijo f o F
Double64 bits3.14Por defecto para decimales
Boolean~1 bittrue / false
Char16 bits'A'Unicode, comilla simple
StringVariable"Hola"Inmutable, array de Chars
val millon = 1_000_000
val hex = 0xFF_EC_DE_5E

val entero: Int = 42
val largo: Long = entero.toLong()

🧪 Cómo probarlo

import kotlin.test.*

class TiposBasicosTest {
    @Test
    fun conversionExplicita() {
        val entero = 42
        val largo = entero.toLong()
        assertIs<Long>(largo)
        assertEquals(42L, largo)
    }

    @Test
    fun underscores() {
        val num = 1_000_000
        assertEquals(1000000, num)
    }
}

💡 Memoria y rendimiento

  • Int, Long, Float, Double no-nulables son primitivos en la JVM
  • Int?, Long? nulables son objetos Integer, Long en el heap
  • String es un objeto en heap con un array char[] interno
  • Las colecciones de primitivos (List<Int>) siempre usan boxing; IntArray no

✅ Buenas prácticas

  • ✅ Usa Int (no Integer) — Kotlin maneja la optimización a primitivo automáticamente
  • ✅ Prefiere String sobre String? cuando el valor nunca será null
  • ⚠️ No uses Float para dinero — prefiere BigDecimal o Int (céntimos)
  • ❌ No asumas conversiones implícitas como en Java — en Kotlin todo es explícito

🏗️ Metodología

  • Para datos numéricos pequeños usa el tipo más pequeño solo si tienes millones de instancias
  • En Android, Int es el tipo más eficiente para IDs y contadores

🔗 Para saber más

Mutabilidad: val vs var

¿Qué es?

val declara una referencia inmutable (read-only, no reasignable), mientras que var declara una referencia mutable (reasignable). val NO significa que el objeto sea inmutable — solo la referencia no se puede reasignar.

🔗 Kotlin docs — Basic Syntax: Variables

Sintaxis básica

val nombre: String = "Ana"
var edad: Int = 30

val lista = mutableListOf(1, 2, 3)
lista.add(4)

🧪 Cómo probarlo

import kotlin.test.*

class MutabilidadTest {
    @Test
    fun valNoReasignable() {
        val x = 42
        assertEquals(42, x)
    }

    @Test
    fun varReasignable() {
        var x = 42
        x = 43
        assertEquals(43, x)
    }

    @Test
    fun valObjetoMutable() {
        val lista = mutableListOf(1, 2, 3)
        lista.add(4)
        assertEquals(listOf(1, 2, 3, 4), lista)
    }
}

💡 Memoria y rendimiento

  • val y var no tienen diferencia en runtime
  • const val (compile-time constant) se inlinea en el código
  • val permite al compilador y al lector razonar mejor sobre el código

✅ Buenas prácticas

  • ✅ Regla de oro: Usa val por defecto. Solo usa var cuando sea estrictamente necesario
  • ⚠️ val no garantiza inmutabilidad profunda — el objeto referenciado puede tener estado mutable
  • ❌ No uses var para colecciones que solo se asignan una vez

🏗️ Metodología

  • En programación funcional, prefiere val + copias (data class copy()) sobre var + mutaciones
  • En Compose, usa val para el estado observable

🔗 Para saber más

Colecciones

¿Qué es?

Kotlin separa claramente colecciones mutables e inmutables (solo lectura) a nivel de tipo. Por defecto, las colecciones son inmutables.

🔗 Kotlin docs — Collections Overview

Sintaxis básica

val lista: List<Int> = listOf(1, 2, 3)
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
mutable.add(4)

val set: Set<Int> = setOf(1, 2, 3, 1, 2)
val mapa: Map<String, Int> = mapOf("uno" to 1, "dos" to 2)

val numeros = listOf(1, 2, 3, 4, 5)
val pares = numeros.filter { it % 2 == 0 }
val cuadrados = numeros.map { it * it }

🧪 Cómo probarlo

import kotlin.test.*

class ColeccionesTest {
    @Test
    fun inmutablePorDefecto() {
        val lista = listOf(1, 2, 3)
        assertEquals(3, lista.size)
    }

    @Test
    fun mutable() {
        val lista = mutableListOf(1, 2, 3)
        lista.add(4)
        assertEquals(listOf(1, 2, 3, 4), lista)
    }

    @Test
    fun operacionesFuncionales() {
        val numeros = listOf(1, 2, 3, 4, 5)
        assertEquals(listOf(2, 4), numeros.filter { it % 2 == 0 })
    }
}

💡 Memoria y rendimiento

  • Acceso por índice en List: O(1) para ArrayList
  • Búsqueda en Set/Map: O(1) promedio (hash)
  • List por defecto es ArrayList (contiguo en memoria)
  • Set/Map por defecto son LinkedHashSet/LinkedHashMap

✅ Buenas prácticas

  • ✅ Declara colecciones como List (inmutable) por defecto
  • ✅ Usa listOf(), setOf(), mapOf() como constructores principales
  • ⚠️ No devuelvas MutableList desde una API pública
  • ❌ No uses LinkedList como predeterminada

🏗️ Metodología

  • En Android/Compose, usa StateFlow<List<T>> + SnapshotStateList para UI reactiva
  • En KMP, las colecciones son exactamente iguales en todas las plataformas

🔗 Para saber más

Nullabilidad

¿Qué es?

El sistema de nullabilidad de Kotlin elimina los NullPointerException en tiempo de compilación. Cualquier tipo puede ser nullable añadiendo ? al tipo. El compilador obliga a manejar el caso null antes de usar el valor.

🔗 Kotlin docs — Null Safety

Sintaxis básica

var nulo: String? = null
var noNulo: String = "Hola"

val longitud = nulo?.length ?: 0
val forzado = nulo!!.length
val numero = texto as? Int ?: 0

if (nulo != null) {
    println(nulo.length)
}

nulo?.let { valor ->
    println("El texto tiene ${valor.length} caracteres")
} ?: println("El texto es null")

🧪 Cómo probarlo

import kotlin.test.*

class NullSafetyTest {
    @Test
    fun safeCall() {
        val nulo: String? = null
        assertEquals(null, nulo?.length)
    }

    @Test
    fun elvisOperator() {
        val nulo: String? = null
        assertEquals(0, nulo?.length ?: 0)
    }

    @Test
    fun smartCast() {
        val x: String? = "hola"
        if (x != null) {
            assertEquals(4, x.length)
        }
    }
}

💡 Memoria y rendimiento

  • String? (nullable) siempre es un objeto en heap, String puede ser optimizado
  • Int? siempre es Integer en heap (boxing), Int es primitivo en stack
  • El smart cast no tiene overhead — es solo azúcar del compilador

✅ Buenas prácticas

  • ✅ Declara tipos no-nulables siempre que sea posible
  • ✅ Usa ?. para acceso seguro y ?: para valores por defecto
  • ✅ Usa let para operaciones encadenadas sobre valores nulables
  • ⚠️ Evita !! en código de producción — es un code smell
  • ❌ No uses Optional de Java en Kotlin

🏗️ Metodología

  • En APIs, devuelve T? en vez de lanzar excepción cuando un valor puede no existir
  • En Android, los Bundle y Intent extras suelen devolver T? — manéjalos con Elvis

🔗 Para saber más

Arrays

¿Qué es?

Los arrays en Kotlin son contenedores de tamaño fijo. Kotlin ofrece arrays genéricos (de objetos) y arrays de primitivos (sin boxing) para rendimiento.

🔗 Kotlin docs — Arrays

Sintaxis básica

val array: Array<String> = arrayOf("a", "b", "c")
val intArray = intArrayOf(1, 2, 3)
val cuadrados = IntArray(5) { i -> i * i }

🧪 Cómo probarlo

import kotlin.test.*

class ArraysTest {
    @Test
    fun arrayGenerico() {
        val arr = arrayOf("a", "b", "c")
        assertEquals("a", arr[0])
    }

    @Test
    fun arrayPrimitivo() {
        val arr = intArrayOf(1, 2, 3)
        assertIs<IntArray>(arr)
    }

    @Test
    fun inicializacionLambda() {
        val cuadrados = IntArray(5) { it * it }
        assertContentEquals(intArrayOf(0, 1, 4, 9, 16), cuadrados)
    }
}

💡 Memoria y rendimiento

  • IntArray: int[] contiguo en heap, 4 bytes por elemento
  • Array<Int>: Integer[] en heap, ~16-24 bytes por elemento (boxing)
  • Para colecciones grandes de primitivos, IntArray es 4-6x más eficiente que List<Int>

✅ Buenas prácticas

  • ✅ Prefiere IntArray/FloatArray sobre Array<Int>/Array<Float> para datos numéricos grandes
  • ⚠️ No uses arrays como reemplazo genérico de listas — las listas son más flexibles
  • ❌ No uses arrays para APIs públicas si puedes usar List

🔗 Para saber más

String templates

¿Qué es?

Kotlin permite incrustar variables y expresiones directamente en cadenas con el prefijo $.

🔗 Kotlin docs — String Templates

Sintaxis básica

val nombre = "Mundo"
println("Hola $nombre!")
println("2 + 2 = ${2 + 2}")
println("Suma: ${sumar(2, 3)}")

🧪 Cómo probarlo

import kotlin.test.*

class StringTemplatesTest {
    @Test
    fun variableSimple() {
        val nombre = "Kotlin"
        assertEquals("Hola Kotlin!", "Hola $nombre!")
    }

    @Test
    fun rawString() {
        val texto = """
            |Línea 1
            |Línea 2
            """.trimMargin()
        assertTrue(texto.contains("Línea 1"))
    }
}

✅ Buenas prácticas

  • ✅ Prefiere templates sobre concatenación con +
  • ✅ Usa raw strings (triple comilla """) para SQL, JSON, HTML multilínea
  • ❌ No anides templates complejos dentro de otros templates

🔗 Para saber más


Control de flujo y modularidad

if como expresión

¿Qué es?

En Kotlin, if no es solo una sentencia — es una expresión que devuelve un valor. No hay operador ternario (cond ? a : b) porque if lo reemplaza.

🔗 Kotlin docs — Conditions

Sintaxis básica

val max = if (a > b) a else b

val resultado = if (condicion) {
    a + b
} else {
    a - b
}

🧪 Cómo probarlo

import kotlin.test.*

class IfExpressionTest {
    @Test
    fun ifDevuelveValor() {
        val max = if (5 > 3) 5 else 3
        assertEquals(5, max)
    }
}

✅ Buenas prácticas

  • ✅ Usa if como expresión para asignaciones condicionales
  • ✅ Prefiere when sobre if-else if encadenados para más de 2 ramas

🔗 Para saber más

when (switch potenciado)

¿Qué es?

when es el reemplazo de Kotlin para switch de Java/C. Es mucho más potente: puede emparejar valores, rangos, tipos, condiciones booleanas, y es exhaustivo con sealed class.

🔗 Kotlin docs — When

Sintaxis básica

val x = 3
val texto = when (x) {
    1 -> "uno"
    2 -> "dos"
    in 3..10 -> "varios"
    !in 0..100 -> "fuera de rango"
    else -> "otro"
}

when {
    x < 0 -> println("Negativo")
    x == 0 -> println("Cero")
    x > 0 -> println("Positivo")
}

sealed class Estado
object Cargando : Estado()
data class Exito(val data: String) : Estado()

fun manejar(estado: Estado) = when (estado) {
    Estado.Cargando -> "⏳"
    is Estado.Exito -> "✅ ${estado.data}"
}

✅ Buenas prácticas

  • ✅ Usa when para 3+ ramas condicionales
  • ✅ Prefiere when exhaustivo con sealed class
  • ⚠️ Siempre incluye else a menos que el when sea exhaustivo

🔗 Para saber más

Bucles: for, while, do-while

for (i in 1..5) print(i)
for (i in 1 until 5) print(i)
for (i in 0..10 step 2) print(i)

val lista = listOf("a", "b", "c")
for (item in lista) print(item)
for ((index, value) in lista.withIndex()) {
    println("$index: $value")
}
lista.forEach { println(it) }

var i = 0
while (i < 5) { println(i); i++ }

var j = 0
do { println(j); j++ } while (j < 5)

Excepciones: try/catch/finally

val resultado = try {
    val dato = api.fetchData()
    "Éxito: $dato"
} catch (e: IOException) {
    "Error de red: ${e.message}"
} catch (e: Exception) {
    "Error: ${e.message}"
} finally {
    println("Operación completada")
}

fun fail(mensaje: String): Nothing {
    throw IllegalStateException(mensaje)
}

val resultado2 = runCatching { api.fetchData() }
    .onSuccess { println("Datos: $it") }
    .onFailure { println("Error: ${it.message}") }

Módulos y organización

package com.ejemplo.util

fun String.contarVocales(): Int = count { it.lowercase() in "aeiou" }

import com.ejemplo.util.contarVocales

Visibilidad:

ModificadorVisibilidad
public (default)Visible en cualquier parte
privateSolo dentro del archivo o clase
protectedClase: visible en subclases
internalVisible dentro del mismo módulo Gradle
// Módulo Gradle separado
plugins { kotlin("jvm") }
dependencies { implementation(kotlin("stdlib")) }

Fuente: Kotlin docs — Control Flow, Kotlin docs — Packages


Sistema de archivos

Lectura y escritura (kotlin.io)

¿Qué es?

Kotlin añade extensiones sobre java.io.File que simplifican la lectura y escritura de archivos. También ofrece APIs multiplataforma con kotlin.io.path para KMP.

🔗 Kotlin docs — Reading/Writing Files

Sintaxis básica

import java.io.File
import kotlin.io.path.*

val file = File("datos.txt")
val texto = file.readText()
val lineas = file.readLines()
file.forEachLine { linea -> println(linea) }
file.writeText("Nuevo contenido")
file.appendText("Más contenido")

import java.nio.file.*
val contenido = Files.readString(Path.of("/ruta/archivo.txt"))
Files.writeString(Path.of("salida.txt"), "Contenido")

// KMP Path
val path = Path("datos.txt")
path.writeText("Contenido")
val leido = path.readText()
path.copyTo(Path("copia.txt"), overwrite = true)

🧪 Cómo probarlo

import kotlin.test.*
import java.io.File
import java.nio.file.Files

class FileIOTest {
    @Test
    fun escribirYLeer() {
        val file = File.createTempFile("test", ".txt")
        file.writeText("Hola Kotlin")
        assertEquals("Hola Kotlin", file.readText())
        file.deleteOnExit()
    }

    @Test
    fun forEachLine() {
        val file = File.createTempFile("test", ".txt")
        file.writeText("línea 1\nlínea 2\nlínea 3")
        val lineas = mutableListOf<String>()
        file.forEachLine { lineas.add(it) }
        assertEquals(3, lineas.size)
        file.deleteOnExit()
    }
}

✅ Buenas prácticas

  • ✅ Usa useLines() para archivos grandes — no carga todo en memoria
  • ✅ Usa Path en KMP y en proyectos nuevos
  • ⚠️ Siempre especifica charset (readText(charset) o Charsets.UTF_8)
  • ❌ No abuses de readLines() en archivos grandes

🔗 Para saber más

Serialización (kotlinx.serialization)

¿Qué es?

kotlinx.serialization es la librería oficial de JetBrains para serializar/deserializar datos. Soporta múltiples formatos (JSON, ProtoBuf, CBOR, HOCON, Properties) y es multiplataforma por defecto.

🔗 kotlinx.serialization docs

Sintaxis básica

// build.gradle.kts
plugins { kotlin("plugin.serialization") version "2.1.0" }
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") }

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class Usuario(
    val nombre: String,
    val email: String,
    val edad: Int = 0,
    @SerialName("es_activo") val activo: Boolean = true
)

val json = Json { prettyPrint = true; ignoreUnknownKeys = true }
val usuario = Usuario("Ana", "ana@email.com", 28)
val stringJson = json.encodeToString(usuario)
val usuarioDes = json.decodeFromString<Usuario>(stringJson)

File("usuario.json").writeText(json.encodeToString(usuario))

🧪 Cómo probarlo

import kotlin.test.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class Persona(val nombre: String, val email: String)

class SerializacionTest {
    @Test
    fun serializarDeserializar() {
        val p = Persona("Ana", "ana@email.com")
        val json = Json.encodeToString(p)
        val recuperado = Json.decodeFromString<Persona>(json)
        assertEquals(p, recuperado)
    }
}

✅ Buenas prácticas

  • ✅ Usa @SerialName para mapear a nombres de campo distintos en JSON
  • ✅ Configura ignoreUnknownKeys = true en APIs externas
  • ⚠️ No serialices clases con referencias circulares
  • ❌ No uses transient de Java — usa @Transient de kotlinx.serialization

🔗 Para saber más

WatchService (monitorización de archivos)

import java.nio.file.*

fun watchDirectory(path: String): Thread = thread {
    val watchService = FileSystems.getDefault().newWatchService()
    val dir = Path.of(path)
    dir.register(watchService,
        StandardWatchEventKinds.ENTRY_CREATE,
        StandardWatchEventKinds.ENTRY_MODIFY,
        StandardWatchEventKinds.ENTRY_DELETE
    )

    while (true) {
        val key = watchService.take()
        key.pollEvents().forEach { event ->
            val filename = event.context() as Path
            when (event.kind()) {
                StandardWatchEventKinds.ENTRY_CREATE -> println("Creado: $filename")
                StandardWatchEventKinds.ENTRY_MODIFY -> println("Modificado: $filename")
                StandardWatchEventKinds.ENTRY_DELETE -> println("Eliminado: $filename")
            }
        }
        key.reset()
    }
}

Algoritmos y estructuras de datos

Ordenación

val numeros = listOf(3, 1, 4, 1, 5, 9, 2, 6)

val ordenada = numeros.sorted()
val descendente = numeros.sortedDescending()

val mutable = numeros.toMutableList()
mutable.sort()

data class Persona(val nombre: String, val edad: Int)
val personas = listOf(Persona("Ana", 28), Persona("Luis", 35))
val porNombre = personas.sortedBy { it.nombre }
val porEdadDesc = personas.sortedByDescending { it.edad }

val multi = personas.sortedWith(
    compareBy<Persona> { it.edad }.thenBy { it.nombre }
)

Búsqueda

val lista = listOf(1, 3, 5, 7, 9, 11, 13)
val indice = lista.binarySearch(7)

data class Item(val id: Int, val nombre: String)
val items = listOf(Item(1, "A"), Item(3, "B"), Item(5, "C"))
val idx = items.binarySearch { it.id.compareTo(3) }

val encontrado = lista.find { it > 5 }

groupingBy, partition, chunked

val palabras = listOf("kotlin", "java", "python", "rust", "go", "c", "ruby")
val porLetra = palabras.groupBy { it.first() }
val conteo = palabras.groupingBy { it.first() }.eachCount()

val numeros = listOf(1, 2, 3, 4, 5, 6)
val (pares, impares) = numeros.partition { it % 2 == 0 }

val grupos = numeros.chunked(3)
val ventanas = numeros.windowed(3)

Tabla Big O

OperaciónArrayListLinkedListHashSetLinkedHashSetHashMap
Acceso (get)O(1)O(n)O(1)*O(1)*O(1)*
Búsqueda (contains)O(n)O(n)O(1)*O(1)*O(1)*
InserciónO(n)O(1)O(1)*O(1)*O(1)*
EliminaciónO(n)O(1)O(1)*O(1)*O(1)*
OrdenaciónO(n log n)O(n log n)———

* Asumiendo buena función hash

Fuente: Kotlin docs — Collections Overview


Conceptos clave explicados a fondo

Null Safety

¿Qué es?

El sistema de nullability de Kotlin distingue en el sistema de tipos entre referencias que pueden ser nulas (T?) y las que no (T). Es la característica que más bugs evita en el día a día. El compilador obliga a manejar el caso null antes de usar el valor.

🔗 Kotlin docs — Null Safety

Sintaxis básica

var noNulo: String = "Hola"
noNulo = null  // ❌ Error de compilación

var siNulo: String? = "Hola"
siNulo = null  // ✅

val texto: String? = obtenerTexto()
val longitud = texto?.length ?: 0
val longitudForzada = texto!!.length
val numero = texto as? Int ?: 0

fun procesar(obj: Any) {
    when (obj) {
        is String -> println(obj.length)
        is Int -> println(obj + 1)
    }
}

texto?.let { valor ->
    println("El texto tiene ${valor.length} caracteres")
} ?: println("El texto es null")

🧪 Cómo probarlo

import kotlin.test.*

class NullSafetyTest {
    @Test
    fun smartCastTrasCheck() {
        val x: String? = "hola"
        if (x != null) {
            assertEquals(4, x.length)
        }
    }

    @Test
    fun elvisConNull() {
        val x: String? = null
        assertEquals("default", x ?: "default")
    }

    @Test
    fun letConNull() {
        var llamado = false
        val x: String? = null
        x?.let { llamado = true }
        assertFalse(llamado)
    }
}

✅ Buenas prácticas

  • ✅ Declara tipos no-nulables por defecto
  • ✅ Usa ?. para acceso seguro y ?: para valores por defecto
  • ✅ Usa let para operaciones encadenadas sobre valores nulables
  • ⚠️ Evita !! — es un code smell
  • ❌ No uses Optional de Java en Kotlin

🔗 Para saber más

Propiedades y backing fields

¿Qué es?

Las propiedades en Kotlin encapsulan getters y setters. El compilador genera automáticamente el campo de respaldo (field) y los accesos. Puedes personalizar getters, setters, visibilidad y añadir validación.

🔗 Kotlin docs — Properties

Sintaxis básica

class Usuario {
    var nombre: String = "Anónimo"
        get() = field.uppercase()
        set(value) {
            field = value.trim()
        }

    var edad: Int = 0
        private set

    lateinit var database: Database

    val config: Config by lazy {
        Config.load()
    }
}

🧪 Cómo probarlo

import kotlin.test.*

class PropiedadesTest {
    @Test
    fun getterPersonalizado() {
        class User {
            var nombre: String = ""
                get() = field.uppercase()
                set(value) { field = value.trim() }
        }
        val user = User()
        user.nombre = "  ana  "
        assertEquals("ANA", user.nombre)
    }

    @Test
    fun lazy() {
        var contador = 0
        val valor by lazy { contador++; 42 }
        assertEquals(42, valor)
        assertEquals(1, contador)
    }
}

✅ Buenas prácticas

  • ✅ Usa private set para propiedades públicas de solo lectura externa
  • ✅ Usa lateinit para inyección de dependencias
  • ✅ Usa by lazy para configuración costosa que no siempre se necesita
  • ⚠️ No accedas a lateinit antes de inicializar

🔗 Para saber más

Data Classes

¿Qué es?

Las data class son clases diseñadas exclusivamente para contener datos. El compilador genera automáticamente equals(), hashCode(), toString(), copy() y componentN() (destructuring).

🔗 Kotlin docs — Data Classes

Sintaxis básica

data class Usuario(
    val nombre: String,
    val email: String,
    val edad: Int = 0,
    val activo: Boolean = true
)

val usuario = Usuario("Ana", "ana@email.com", 28)
val usuario2 = usuario.copy(edad = 30)
val (nombre, email) = usuario

🧪 Cómo probarlo

import kotlin.test.*

class DataClassTest {
    @Test
    fun equalsPorValor() {
        val a = Usuario("Ana", "a@b.com", 28)
        val b = Usuario("Ana", "a@b.com", 28)
        assertEquals(a, b)
    }

    @Test
    fun copyModifica() {
        val original = Usuario("Ana", "a@b.com", 28)
        val copia = original.copy(edad = 30)
        assertEquals(30, copia.edad)
    }

    @Test
    fun destructuring() {
        val usuario = Usuario("Ana", "a@b.com", 28)
        val (nombre, email) = usuario
        assertEquals("Ana", nombre)
    }
}

✅ Buenas prácticas

  • ✅ Usa data class para modelos, DTOs, eventos
  • ⚠️ No heredes de data classes
  • ❌ No añadas lógica de negocio en data classes

🔗 Para saber más

Sealed Classes

¿Qué es?

Las sealed class representan jerarquías de clases restringidas: todas las subclases posibles se conocen en tiempo de compilación. Son ideales para representar estados, resultados, o uniones discriminadas.

🔗 Kotlin docs — Sealed Classes

Sintaxis básica

sealed class Resultado<out T> {
    data class Exito<T>(val datos: T) : Resultado<T>()
    data class Error(val mensaje: String, val codigo: Int = 0) : Resultado<Nothing>()
    object Cargando : Resultado<Nothing>()
}

fun <T> procesar(resultado: Resultado<T>): String = when (resultado) {
    is Resultado.Exito -> "Éxito: ${resultado.datos}"
    is Resultado.Error -> "Error: ${resultado.mensaje} (código ${resultado.codigo})"
    Resultado.Cargando -> "Cargando..."
}

sealed interface Estado {
    data class Activo(val desde: Long) : Estado
    data class Inactivo(val motivo: String) : Estado
    object Pendiente : Estado
}

✅ Buenas prácticas

  • ✅ Usa sealed class para estados de UI (Loading, Success, Error)
  • ✅ Usa sealed class para Result<T> funcional
  • ⚠️ Las subclases deben estar en el mismo archivo o paquete

🔗 Para saber más

Object, Companion Object y Object Expressions

object Config {
    val version = "1.0"
    fun cargar() { println("Cargando configuración...") }
}

class Usuario(val nombre: String) {
    companion object Factory {
        fun crear(nombre: String): Usuario = Usuario(nombre.trim())
        val anonimo = Usuario("Anónimo")
    }
}

val listener = object : ClickListener {
    override fun onClick() { println("Clickeado!") }
}

🔗 Kotlin docs — Object

Extension Functions

¿Qué es?

Las extension functions permiten añadir métodos a clases existentes sin herencia ni decoradores. Se resuelven estáticamente (no son polimórficas).

🔗 Kotlin docs — Extensions

Sintaxis básica

fun String.esPalindromo(): Boolean = this == this.reversed()

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

val <T> List<T>.ultimo: T?
    get() = if (isEmpty()) null else this[lastIndex]

infix fun Int.esMultiploDe(otro: Int): Boolean = this % otro == 0

✅ Buenas prácticas

  • ✅ Usa extension functions para añadir comportamiento a clases que no puedes modificar
  • ⚠️ No declares extension functions que oculten métodos miembros
  • ❌ No abuses: si usas la misma extensión en 10+ archivos, considera una librería

🔗 Para saber más

Operator Overloading

data class Punto(val x: Int, val y: Int) {
    operator fun plus(otro: Punto) = Punto(x + otro.x, y + otro.y)
    operator fun minus(otro: Punto) = Punto(x - otro.x, y - otro.y)
    operator fun times(escalar: Int) = Punto(x * escalar, y * escalar)
}

val p1 = Punto(3, 4)
val p2 = Punto(1, 2)
println(p1 + p2)

class Operacion {
    operator fun invoke(a: Int, b: Int): Int = a + b
}
val suma = Operacion()
println(suma(3, 4))

🔗 Kotlin docs — Operator Overloading

Scope Functions

FunciónContextoRetornoCaso de uso
letitResultadoOperaciones con null safety
runthisResultadoComputar un valor
withthisResultadoConfigurar sin retornar
applythisObjeto mismoConfigurar objetos
alsoitObjeto mismoEfectos secundarios
data class Persona(var nombre: String = "", var edad: Int = 0)

val persona = Persona().apply {
    nombre = "Ana"
    edad = 28
}

val texto: String? = "Hola"
val longitud = texto?.let { it.length } ?: 0

val info = persona.run { "$nombre tiene $edad años" }

val lista = mutableListOf(1, 2, 3).also {
    println("Creando lista con ${it.size} elementos")
}

🔗 Kotlin docs — Scope Functions

Genéricos: Reified, Variance y Proyecciones

inline fun <reified T> esTipo(obj: Any): Boolean = obj is T

interface Productor<out T> { fun obtener(): T }
interface Consumidor<in T> { fun procesar(valor: T) }

val strings: List<String> = listOf("a", "b")
val any: List<Any> = strings

fun <T> clonarIfComparable(item: T): T
    where T : Comparable<T>, T : Cloneable = item

🔗 Kotlin docs — Generics

DSL Builders con Receiver

class HTML {
    private val children = mutableListOf<String>()
    fun body(init: BODY.() -> Unit) {
        val body = BODY(); body.init(); children.add(body.toString())
    }
    override fun toString() = "<html>${children.joinToString("")}</html>"
}

class BODY {
    private val children = mutableListOf<String>()
    fun h1(text: String) { children.add("<h1>$text</h1>") }
    fun p(text: String) { children.add("<p>$text</p>") }
    override fun toString() = "<body>${children.joinToString("")}</body>"
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML(); html.init(); return html
}

val pagina = html {
    body {
        h1("Título")
        p("Esto es un párrafo")
    }
}

🔗 Kotlin docs — Type-Safe Builders

inline, noinline, crossinline

inline fun <T> medirTiempo(block: () -> T): T {
    val inicio = System.nanoTime()
    val resultado = block()
    println("Tardó ${(System.nanoTime() - inicio) / 1_000_000}ms")
    return resultado
}

inline fun operacion(
    block1: () -> Unit,
    noinline block2: () -> Unit
) {
    block1()
    ejecutarLater(block2)
}

inline fun operar(crossinline block: () -> Unit) {
    ejecutarEnOtroContexto { block() }
}

Delegados (by)

val config: Config by lazy { Config.load() }

var nombre: String by Delegates.observable("inicial") { _, old, new ->
    println("Cambió de '$old' a '$new'")
}

var edad: Int by Delegates.vetoable(0) { _, _, new -> new >= 0 }

class Usuario(map: Map<String, Any?>) {
    val nombre: String by map
    val edad: Int by map
}

class Trimmed {
    private var value: String = ""
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        value = newValue.trim()
    }
}
var texto: String by Trimmed()

🔗 Kotlin docs — Delegation


POO y patrones de diseño

Sistema de biblioteca multimedia (ejemplo completo)

import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

data class Metadatos(
    val titulo: String,
    val duracionSegundos: Int,
    val tags: List<String> = emptyList(),
    val fechaCreacion: LocalDateTime = LocalDateTime.now()
) {
    val duracionFormateada: String
        get() {
            val min = duracionSegundos / 60
            val seg = duracionSegundos % 60
            return "$min:${seg.toString().padStart(2, '0')}"
        }
}

sealed class EstadoReproduccion {
    data class Reproduciendo(val desde: LocalDateTime) : EstadoReproduccion()
    data class Pausado(val progreso: Int) : EstadoReproduccion()
    object Detenido : EstadoReproduccion()
    data class Error(val mensaje: String) : EstadoReproduccion()
}

interface Contenido {
    val metadatos: Metadatos
    var estado: EstadoReproduccion
    fun reproducir(): String
    fun obtenerInfo(): String
    fun duracionFormateada(): String = metadatos.duracionFormateada
}

class Audio(override val metadatos: Metadatos, val bitrateKbps: Int) : Contenido {
    override var estado: EstadoReproduccion = EstadoReproduccion.Detenido
    private var reproducciones: Int = 0

    override fun reproducir(): String {
        reproducciones++
        estado = EstadoReproduccion.Reproduciendo(LocalDateTime.now())
        return "▶️ Reproduciendo ${metadatos.titulo}..."
    }

    override fun obtenerInfo(): String =
        "🎵 ${metadatos.titulo} | ${metadatos.duracionFormateada} | ${bitrateKbps}kbps"
}

class Video(override val metadatos: Metadatos, val resolucion: String, val formato: String) : Contenido {
    override var estado: EstadoReproduccion = EstadoReproduccion.Detenido
    var subtitulos: Boolean = false

    override fun reproducir(): String {
        estado = EstadoReproduccion.Reproduciendo(LocalDateTime.now())
        return "▶️ Reproduciendo ${metadatos.titulo}..."
    }

    override fun obtenerInfo(): String =
        "🎬 ${metadatos.titulo} | $resolucion | $formato | ${if (subtitulos) "CC" else "Sin subtítulos"}"
}

interface Exportable {
    fun exportar(formato: String = "json"): String {
        return if (formato == "json") {
            "{\"tipo\": \"${this::class.simpleName}\", \"titulo\": \"${(this as? Contenido)?.metadatos?.titulo}\"}"
        } else {
            "Exportación a $formato no soportada"
        }
    }
}

object Logger {
    var nivel: String = "INFO"
    private val logs = mutableListOf<String>()
    fun log(mensaje: String) {
        val entrada = "[${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)}] [$nivel] $mensaje"
        logs.add(entrada)
        println(entrada)
    }
    fun historial(): List<String> = logs.toList()
}

class Podcast(metadatos: Metadatos, bitrateKbps: Int, val episodio: Int, val anfitrion: String)
    : Contenido, Exportable {
    private val audio = Audio(metadatos, bitrateKbps)
    override val metadatos: Metadatos get() = audio.metadatos
    override var estado: EstadoReproduccion
        get() = audio.estado
        set(value) { audio.estado = value }
    override fun reproducir(): String = audio.reproducir()
    override fun obtenerInfo(): String =
        "🎙️ Ep.$episodio: ${metadatos.titulo} | con $anfitrion"
    fun emitir() { Logger.log("Emitiendo podcast episodio $episodio con $anfitrion") }
}

class ListaReproduccion<T : Contenido>(val nombre: String) {
    private val items = mutableListOf<T>()
    fun agregar(item: T) { items.add(item) }
    fun reproducirTodo(): List<String> = items.map { it.reproducir() }
    fun infoCompleta(): List<String> = items.map { it.obtenerInfo() }
    val size: Int get() = items.size
    operator fun get(indice: Int): T = items[indice]
    override fun toString(): String = "📋 $nombre ($size items)"
}

fun main() = runBlocking {
    val cancion = Audio(Metadatos("Bohemian Rhapsody", 354, tags = listOf("rock")), bitrateKbps = 320)
    val pelicula = Video(Metadatos("Inception", 8880), resolucion = "4K", formato = "MKV").apply {
        subtitulos = true
    }

    listOf(cancion, pelicula).forEach { println(it.obtenerInfo()) }

    val lista = ListaReproduccion("Favoritos")
    lista.agregar(cancion)
    lista.agregar(pelicula)
    println(lista.reproducirTodo())
}

Conceptos demostrados:

ConceptoCómo aparece
InterfaceContenido con propiedades abstractas y método concreto
Data classMetadatos con copy(), destructuring
Sealed classEstadoReproduccion con pattern matching exhaustivo
Object (singleton)Logger con estado global
ComposiciónPodcast delega en Audio
GenéricosListaReproduccion<T : Contenido> con bound superior
Operator overloadingoperator fun get()
Scope functionsapply para configurar pelicula
Herencia múltiple vía interfacesPodcast implementa Contenido y Exportable

Patrones de diseño en Kotlin

PatrónCómo se implementa en KotlinEjemplo
StrategyLambdas + typealiastypealias Estrategia = (List<Int>) -> List<Int>
ObserverDelegates.observable / StateFlowvar x by Delegates.observable(init) { ... }
Singletonobjectobject Config { ... }
Factorycompanion object + extensionfun Procesador.Companion.crear(...)
Builderapply + DSLPersona().apply { nombre = "Ana" }
AdapterExtension functionsfun SistemaViejo.comoNuevo()

Strategy con funciones

typealias EstrategiaOrdenacion = (List<Int>) -> List<Int>
val ascendente: EstrategiaOrdenacion = { it.sorted() }
val descendente: EstrategiaOrdenacion = { it.sortedDescending() }

class Analizador(private val estrategia: EstrategiaOrdenacion) {
    fun analizar(datos: List<Int>): List<Int> = estrategia(datos)
}

Builder con apply

data class Cafe(val tipo: String = "expresso", val tamano: String = "mediano",
    val leche: String = "entera", val extras: List<String> = emptyList())

fun cafe(init: CafeBuilder.() -> Unit): Cafe = CafeBuilder().apply(init).build()

class CafeBuilder {
    var tipo: String = "expresso"
    var tamano: String = "mediano"
    var leche: String = "entera"
    private val extras = mutableListOf<String>()
    fun extra(extra: String) { extras.add(extra) }
    fun build(): Cafe = Cafe(tipo, tamano, leche, extras)
}

val miCafe = cafe { tipo = "latte"; tamano = "grande"; leche = "avena"; extra("canela") }

🔗 Para saber más


Polimorfismo en detalle

¿Qué es?

Kotlin tiene tres formas de polimorfismo que coexisten en el mismo código: polimorfismo por subtipo (POO clásico), polimorfismo paramétrico (genéricos), y polimorfismo ad-hoc (extension functions).

🔗 Kotlin docs — Polymorphism

1. Polimorfismo por subtipo

interface Animal {
    fun sonido(): String
}

class Perro : Animal {
    override fun sonido(): String = "Guau"
}

class Gato : Animal {
    override fun sonido(): String = "Miau"
}

fun hacerSonido(animal: Animal) {
    println(animal.sonido())
}

hacerSonido(Perro())  // Guau
hacerSonido(Gato())   // Miau

2. Polimorfismo paramétrico (genéricos)

interface Caja<T> {
    fun obtener(): T
    fun guardar(item: T)
}

class CajaSimple<T>(private var item: T) : Caja<T> {
    override fun obtener(): T = item
    override fun guardar(item: T) { this.item = item }
}

fun <T : Comparable<T>> maximo(a: T, b: T): T = if (a > b) a else b
println(maximo(3, 7))
println(maximo("Ana", "Luis"))

3. Polimorfismo ad-hoc (extension functions)

interface Sonoro {
    fun sonido(): String
}

fun Sonoro.describir(): String = "Este animal hace: ${sonido()}"

class Perro : Sonoro {
    override fun sonido(): String = "Guau"
}

fun describirAnimal(animal: Sonoro) {
    println(animal.describir())
}

fun Int.describir(): String = "Número: $this"
fun String.describir(): String = "Texto: $this"
println(42.describir())
println("hola".describir())
TipoJavaKotlinSwift
Subtipointerface + implementsinterface + :protocol + :
Paramétrico<T> (type erasure)<T> (reified en inline)<T>
Ad-hocNo directoExtension functionsProtocol Extensions

🔗 Para saber más


Interacción con contenido multimedia

En JVM (Java 2D, ImageIO, JavaFX)

import java.awt.*
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import java.io.File

fun crearImagen(): BufferedImage {
    val img = BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB)
    val g = img.createGraphics()
    g.color = Color.WHITE; g.fillRect(0, 0, 400, 300)
    g.color = Color.RED; g.fillOval(50, 50, 100, 100)
    g.color = Color.BLUE; g.font = Font("Arial", Font.BOLD, 24)
    g.drawString("Hola Kotlin", 150, 150)
    g.dispose()
    return img
}

ImageIO.write(crearImagen(), "png", File("salida.png"))

val imagen = ImageIO.read(File("foto.jpg"))
println("Ancho: ${imagen.width}, Alto: ${imagen.height}")

JavaFX (aplicaciones desktop):

// build.gradle.kts
plugins { id("org.openjfx.javafxplugin") version "0.1.0" }
javafx { version = "21"; modules = listOf("javafx.controls", "javafx.media") }

import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.media.Media
import javafx.scene.media.MediaPlayer
import javafx.scene.layout.StackPane
import javafx.stage.Stage
import java.io.File

class ReproductorAudio : Application() {
    override fun start(stage: Stage) {
        val media = Media(File("audio.mp3").toURI().toString())
        val player = MediaPlayer(media)
        player.play()
        stage.scene = Scene(StackPane(), 200.0, 100.0)
        stage.show()
    }
}

fun main() { Application.launch(ReproductorAudio::class.java) }

Multiplataforma

PlataformaAPI principal
JVMJava 2D / JavaFX / Swing
AndroidBitmap / Canvas / OpenGL ES
iOSCore Graphics / Metal / AVFoundation
Web (JS)Canvas API / WebGL

Android

Bitmap y Canvas:

import android.graphics.*

val bitmap = Bitmap.createBitmap(400, 300, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)

val paint = Paint().apply {
    color = Color.RED
    style = Paint.Style.FILL
    textSize = 48f
}

canvas.drawCircle(200f, 150f, 100f, paint)
canvas.drawText("Hola", 160f, 160f, Paint().apply {
    color = Color.WHITE; textSize = 32f
})

MediaPlayer y ExoPlayer:

import android.media.MediaPlayer
val player = MediaPlayer.create(context, R.raw.mi_audio)
player.start()
player.release()

// ExoPlayer (moderno)
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
val exoPlayer = ExoPlayer.Builder(context).build()
exoPlayer.setMediaItem(MediaItem.fromUri("https://example.com/video.mp4"))
exoPlayer.prepare()
exoPlayer.play()

Coil (carga de imágenes Kotlin-first):

// build.gradle.kts
dependencies { implementation("io.coil-kt:coil:2.6.0") }

import coil.load

imageView.load("https://example.com/imagen.jpg") {
    placeholder(R.drawable.placeholder)
    error(R.drawable.error)
    crossfade(true)
    size(400, 300)
}

Fuente: Android Developer — Graphics, Coil docs


Bases de datos

SQL

LibreríaAsyncORMPara qué
ExposedSí (corrutinas)DSL + ORM ligeroFramework oficial de JetBrains
RoomSí (corrutinas)ORMEstándar Android con DAOs anotados
SQLDelightSíGenera Kotlin desde SQLMultiplataforma
Spring Data JPASí (WebFlux)ORM completoEcosistema Spring Boot
// Exposed (DSL)
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

object Usuarios : Table() {
    val id = integer("id").autoIncrement()
    val nombre = varchar("nombre", 100)
    val email = varchar("email", 255).uniqueIndex()
    override val primaryKey = PrimaryKey(id)
}

fun main() {
    Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
    transaction {
        SchemaUtils.create(Usuarios)
        Usuarios.insert { it[nombre] = "Ana"; it[email] = "ana@email.com" }
        val usuarios = Usuarios.selectAll().toList()
        println(usuarios)
    }
}

// Room (Android)
@Entity
data class Usuario(@PrimaryKey val id: Int, val nombre: String, val email: String)

@Dao
interface UsuarioDao {
    @Query("SELECT * FROM usuario")
    suspend fun obtenerTodos(): List<Usuario>

    @Insert
    suspend fun insertar(usuario: Usuario)
}

NoSQL

LibreríaBDAsync
MongoDB Kotlin DriverMongoDBSí
kmongoMongoDBSí
redis-kotlinRedisSí
// MongoDB Kotlin Driver
val client = MongoClient.connect("mongodb://localhost:27017")
val db = client.getDatabase("app")
val coleccion = db.getCollection<Document>("usuarios")

suspend fun insertar(nombre: String, email: String) {
    coleccion.insertOne(Document("nombre", nombre).append("email", email))
}

Fuente: Exposed docs, SQLDelight docs


WebSockets y mensajería

LibreríaPara qué
Ktor WebSocketServidor/cliente WebSocket nativo
Spring WebSocketIntegrado en Spring Boot
OkHttp WebSocketCliente para Android/JVM
// Ktor WebSocket (servidor)
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.websocket.*
import io.ktor.http.*
import io.ktor.server.routing.*
import io.ktor.server.plugins.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        install(WebSockets)
        routing {
            webSocket("/chat") {
                send(Frame.Text("Bienvenido al chat!"))
                for (frame in incoming) {
                    if (frame is Frame.Text) {
                        val text = frame.readText()
                        send(Frame.Text("Echo: $text"))
                    }
                }
            }
        }
    }.start(wait = true)
}

// OkHttp WebSocket (cliente Android)
import okhttp3.*

val client = OkHttpClient()
val request = Request.Builder().url("ws://echo.websocket.org").build()
val ws = client.newWebSocket(request, object : WebSocketListener() {
    override fun onOpen(webSocket: WebSocket, response: Response) {
        webSocket.send("Hola servidor!")
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        println("Recibido: $text")
    }
})

// Spring WebSocket (servidor)
@Configuration
@EnableWebSocket
class WebSocketConfig : WebSocketConfigurer {
    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry.addHandler(MiHandler(), "/ws").setAllowedOrigins("*")
    }
}

class MiHandler : TextWebSocketHandler() {
    private val sessions = mutableSetOf<WebSocketSession>()

    override fun afterConnectionEstablished(session: WebSocketSession) {
        sessions.add(session)
        session.sendMessage(TextMessage("Conectado!"))
    }

    override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
        sessions.forEach { it.sendMessage(TextMessage("Echo: ${message.payload}")) }
    }
}

Fuente: Ktor WebSocket docs, OkHttp WebSocket


Concurrencia y paralelismo

launch vs async

fun main() = runBlocking {
    val job: Job = launch {
        delay(1000)
        println("Tarea completada")
    }
    job.join()

    val deferred: Deferred<String> = async {
        delay(500)
        "Resultado"
    }
    println(deferred.await())

    val resultados = listOf(
        async { fetchUserData() },
        async { fetchPosts() },
        async { fetchComments() }
    )
    resultados.awaitAll()
}

Structured Concurrency

fun main() = runBlocking {
    try {
        coroutineScope {
            launch { delay(1000); println("Tarea 1") }
            launch { delay(500); throw RuntimeException("Error en tarea 2") }
        }
    } catch (e: Exception) {
        println("Ámbito cancelado: ${e.message}")
    }
}

supervisorScope {
    launch { delay(1000); println("Tarea 1") }
    launch { delay(500); throw RuntimeException("Error") }
}

Dispatchers

DispatcherPool de hilosUso típico
Dispatchers.DefaultNúcleos CPU (mínimo 2)Cómputo, parsing
Dispatchers.IO64 hilos (por defecto)Red, BD, archivos
Dispatchers.MainUI threadAndroid: actualizar vistas
Dispatchers.UnconfinedEl hilo actualTesting, prototipos
withContext(Dispatchers.Default) { procesarDatos(datos) }
withContext(Dispatchers.IO) { api.fetchUsers() }

Flow (cold async streams)

fun flujoDatos(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}.flowOn(Dispatchers.Default)

fun main() = runBlocking {
    flujoDatos()
        .map { it * 2 }
        .filter { it > 4 }
        .catch { e -> println("Error: $e") }
        .collect { println(it) }
}

StateFlow vs SharedFlow (hot streams)

class ViewModel {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    private val _eventos = MutableSharedFlow<UiEvent>()
    val eventos: SharedFlow<UiEvent> = _eventos.asSharedFlow()

    fun cargarDatos() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val datos = api.fetchData()
                _uiState.value = UiState.Success(datos)
                _eventos.emit(UiEvent.DatosCargados)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Error")
            }
        }
    }
}

Channel (comunicación productor-consumidor)

val channel = Channel<String>(capacity = Channel.BUFFERED)

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        repeat(5) {
            channel.send("Mensaje $it")
            delay(100)
        }
        channel.close()
    }

    for (msg in channel) {
        println("Recibido: $msg")
    }
}

Tabla de usos

EscenarioHerramienta
Llamada a APIasync / withContext(IO)
Tareas paralelas CPUasync en Dispatchers.Default
Stream de datos en tiempo realFlow
Estado de UIStateFlow
Eventos de un solo disparoSharedFlow
Cola productor/consumidorChannel

Manejo de excepciones

fun main() = runBlocking {
    val deferred = async { throw RuntimeException("Error") }
    try { deferred.await() }
    catch (e: Exception) { println("Capturado: ${e.message}") }

    val handler = CoroutineExceptionHandler { _, exception ->
        println("Handler capturó: ${exception.message}")
    }

    supervisorScope {
        launch { throw RuntimeException("Error 1") }
        launch { delay(200); println("Tarea 2 completada") }
    }
}

¿Cómo se compara?

  • Threads (Java): Pesados (~1MB/hilo), difíciles de cancelar. Corrutinas: ~100 bytes/cada una
  • CompletableFuture (Java): Verboso, sin structured concurrency
  • async/await (JS/TS): Similar sintaxis, pero Kotlin añade Flow, Channel, StateFlow, Dispatchers
  • Swift concurrency: async/await + Task + AsyncSequence — muy similar conceptualmente

Fuente: Kotlin docs — Coroutines, Kotlin Coroutines Guide


Testing y calidad

Frameworks y herramientas

FrameworkPropósitoAsyncCLI
kotlin.testUnitario (JUnit 5)Sígradle test
KotestBDD, property-basedSígradle test
MockKMockingSígradle test
KoverCobertura—gradle koverReport

Cómo testear cada concepto

import kotlin.test.*

// Variables y tipos — aserciones simples
@Test
fun tiposBasicos() {
    val x = 42
    assertIs<Int>(x)
    assertEquals(42, x)
}

// Control de flujo — cobertura de ramas
@Test
fun whenExhaustivo() {
    fun clasificar(x: Int) = when {
        x < 0 -> "negativo"
        x == 0 -> "cero"
        else -> "positivo"
    }
    assertEquals("negativo", clasificar(-1))
    assertEquals("cero", clasificar(0))
    assertEquals("positivo", clasificar(1))
}

// POO — mocks con MockK
@Test
fun mockConMockK() {
    val repo = mockk<Repositorio> {
        every { obtenerDatos() } returns listOf("test")
    }
    assertEquals(1, repo.obtenerDatos().size)
}

// Corrutinas — runTest
@Test
fun corrutinas() = runTest {
    val resultado = async { "test" }
    assertEquals("test", resultado.await())
}

// File I/O — temp directories
@Test
fun archivos() {
    val dir = createTempDir()
    val file = File(dir, "test.txt")
    file.writeText("contenido")
    assertTrue(file.exists())
    assertEquals("contenido", file.readText())
    dir.deleteRecursively()
}

Parametrización

// JUnit 5 parametrizado
@ParameterizedTest
@ValueSource(ints = [1, 2, 3, 4, 5])
fun esPositivo(numero: Int) {
    assertTrue(numero > 0)
}

// Kotest property-based
class PropTest : StringSpec({
    "reverse debería mantener longitud" {
        forAll<String> { str ->
            str.reversed().length == str.length
        }
    }
})

Cobertura y CI

# Kover (oficial JetBrains)
gradle koverReport

# JaCoCo (alternativa)
gradle jacocoTestReport

# GitHub Actions básico
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { java-version: '21', distribution: 'temurin' }
      - run: gradle test --no-daemon

Fuente: Kotlin docs — Testing, Kotest docs, MockK docs


Conceptos avanzados para Kotlin

Corrutinas: Flow y Channels avanzado

Flow: Stream frío que emite valores bajo demanda. Es backpressure-aware y se integra con structured concurrency.

// Flow con combinación de fuentes
data class User(val id: Int, val name: String)
data class Order(val userId: Int, val total: Double)

fun getUsers(): Flow<User> = flow {
    emit(User(1, "Ana")); emit(User(2, "Luis"))
}

fun getOrders(): Flow<Order> = flow {
    emit(Order(1, 100.0)); emit(Order(1, 50.0)); emit(Order(2, 200.0))
}

// Combinar flujos
fun main() = runBlocking {
    getUsers().flatMapConcat { user ->
        getOrders().filter { it.userId == user.id }
            .map { "${user.name}: ${it.total}" }
    }.collect { println(it) }
}

// Flow con buffer y conflate
flow {
    for (i in 1..10) { emit(i); delay(100) }
}.buffer(10)
 .conflate()
 .collect { println(it) }

Channels: Comunicación productor-consumidor con diferentes tipos de capacidad.

// Channel RENDEZVOUS (sin buffer, bloqueante)
val rendezvous = Channel<String>(Channel.RENDEZVOUS)

// Channel BUFFERED (buffer fijo)
val buffered = Channel<String>(10)

// Channel CONFLATED (solo el último valor)
val conflated = Channel<String>(Channel.CONFLATED)

// Channel UNLIMITED (sin límite, cuidado con OOM)
val unlimited = Channel<String>(Channel.UNLIMITED)

// Fan-out: múltiples consumidores
fun main() = runBlocking {
    val channel = Channel<Int>(10)
    repeat(3) { id ->
        launch {
            for (msg in channel) {
                println("Worker $id: $msg")
            }
        }
    }
    repeat(10) { channel.send(it) }
    channel.close()
}

Kotlin Multiplatform (KMP)

Comparte lógica de negocio entre Android, iOS, web y desktop desde un mismo código.

// build.gradle.kts (módulo KMP)
plugins {
    kotlin("multiplatform") version "2.1.0"
    kotlin("plugin.serialization") version "2.1.0"
}

kotlin {
    androidTarget {
        compilations.all { kotlinOptions.jvmTarget = "17" }
    }

    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
        it.binaries.framework { baseName = "shared" }
    }

    sourceSets {
        commonMain.dependencies {
            implementation(kotlin("stdlib-common"))
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
        }
        androidMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
        }
    }
}

// commonMain — código compartido
expect fun platformName(): String

class Greeting {
    fun greet(): String = "Hola desde ${platformName()}"
}

// androidMain — implementación Android
actual fun platformName(): String = "Android"

// iosMain — implementación iOS
actual fun platformName(): String = "iOS"

Compose Multiplatform

UI declarativa compartida entre Android, iOS, Desktop y Web.

@Composable
fun App() {
    var count by remember { mutableStateOf(0) }

    MaterialTheme {
        Column(
            modifier = Modifier.fillMaxSize().padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text("Has pulsado $count veces")
            Button(onClick = { count++ }) {
                Text("Pulsar")
            }
        }
    }
}

// main (Android + Desktop)
fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        App()
    }
}

Conceptos clave de Compose:

  • @Composable: función que describe la UI
  • State: estado observable que provoca recomposición
  • remember: mantiene estado entre recomposiciones
  • LaunchedEffect: lanza corrutinas en composables
  • Modifier: cadena de modificadores (tamaño, padding, click)

🔗 Para saber más


Proyecto final integrador: API de recetas con Ktor

Este proyecto combina file system, algoritmos, POO, testing y red.

// ── Modelos (POO + data class) ──
data class Ingrediente(val nombre: String, val cantidad: Double, val unidad: String)

data class Receta(
    val id: Int,
    val titulo: String,
    val ingredientes: List<Ingrediente>,
    val instrucciones: String,
    val tiempoMinutos: Int
)

// ── Repositorio (file system + algoritmos) ──
class RepositorioRecetas(private val archivo: java.io.File) {
    private val recetas = mutableListOf<Receta>()

    init {
        if (archivo.exists()) {
            archivo.forEachLine { linea ->
                val partes = linea.split("|")
                if (partes.size >= 4) {
                    recetas.add(Receta(
                        id = partes[0].toIntOrNull() ?: 0,
                        titulo = partes[1],
                        ingredientes = emptyList(),
                        instrucciones = partes[2],
                        tiempoMinutos = partes[3].toIntOrNull() ?: 0
                    ))
                }
            }
        }
    }

    fun buscarPorTiempo(maxMinutos: Int): List<Receta> =
        recetas.filter { it.tiempoMinutos <= maxMinutos }
            .sortedBy { it.tiempoMinutos }

    fun buscarPorTexto(query: String): List<Receta> =
        recetas.filter {
            it.titulo.contains(query, ignoreCase = true) ||
            it.instrucciones.contains(query, ignoreCase = true)
        }

    fun agregar(receta: Receta) {
        recetas.add(receta)
        guardar()
    }

    private fun guardar() {
        archivo.writeText(recetas.joinToString("\n") { r ->
            "${r.id}|${r.titulo}|${r.instrucciones}|${r.tiempoMinutos}"
        })
    }
}

// ── API REST con Ktor ──
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.request.*

fun main() {
    val repo = RepositorioRecetas(java.io.File("recetas.txt"))

    embeddedServer(Netty, port = 8080) {
        routing {
            get("/recetas") {
                val maxTiempo = call.request.queryParameters["maxTiempo"]?.toIntOrNull()
                val query = call.request.queryParameters["q"]

                val resultado = when {
                    maxTiempo != null -> repo.buscarPorTiempo(maxTiempo)
                    query != null -> repo.buscarPorTexto(query)
                    else -> emptyList()
                }
                call.respond(resultado)
            }

            post("/recetas") {
                val receta = call.receive<Receta>()
                repo.agregar(receta)
                call.respond(HttpStatusCode.Created, receta)
            }
        }
    }.start(wait = true)
}

Tests

import kotlin.test.*
import java.io.File
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.testing.*
import io.ktor.http.*

class RecetasTest {
    @Test
    fun repositorioPersiste() {
        val file = File.createTempFile("recetas", ".txt")
        val repo = RepositorioRecetas(file)
        repo.agregar(Receta(1, "Tortilla", emptyList(), "Cocinar", 10))

        val repo2 = RepositorioRecetas(file)
        assertEquals(1, repo2.buscarPorTexto("Tortilla").size)
        file.deleteOnExit()
    }

    @Test
    fun apiGetRecetas() = testApplication {
        application { /* configurar módulo Ktor */ }
        val response = client.get("/recetas?q=pollo")
        assertEquals(HttpStatusCode.OK, response.status)
    }
}

Despliegue a producción

Build para producción

./gradlew build -x test

El JAR se genera en build/libs/.

Docker multi-stage

FROM gradle:8-jdk21 AS builder
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
RUN gradle dependencies --no-daemon
COPY src ./src
RUN gradle build -x test --no-daemon

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
CMD ["java", "-jar", "app.jar"]

Hosting

  • Fly.io — despliegue con flyctl launch
  • Railway — conecta tu repo y despliega automáticamente
  • Heroku — despliegue con Git o Container Registry

CI/CD (GitHub Actions)

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - run: ./gradlew build -x test
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Canales y recursos en español

YouTube

  • Kotlin en Español — Curso desde cero, corrutinas, Android, Ktor
  • Midulive — Kotlin, desarrollo multiplataforma, proyectos prácticos
  • Develoteca — Kotlin para Android, Jetpack Compose, MVVM
  • CodigoFacilito — Kotlin desde fundamentos hasta apps Android
  • Programación ATS — Curso de Kotlin básico
  • MoureDev — Kotlin, Android y desarrollo de apps
  • Platzi — Ruta de Kotlin con proyectos Android

Comunidades

  • r/Kotlin (Reddit) — Noticias, discusiones, proyectos
  • Kotlin en Español (Telegram) — Grupo activo de ayuda
  • Kotlin Developers (Discord) — Comunidad internacional
  • Stack Overflow en español — Etiqueta kotlin

Repositorios

Blogs y newsletters


Hacks y tips de productividad

1. Type-safe builders con apply

val alertDialog = AlertDialog.Builder(context).apply {
    setTitle("Confirmar")
    setMessage("¿Estás seguro?")
    setPositiveButton("Sí") { _, _ -> accion() }
    setNegativeButton("No", null)
}.create()

2. Sealed class + when exhaustivo para estados de UI

sealed class UiState {
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
    object Loading : UiState()
}

fun render(state: UiState) = when (state) {
    is UiState.Success -> mostrarLista(state.data)
    is UiState.Error -> mostrarError(state.message)
    UiState.Loading -> mostrarSpinner()
}

3. inline + reified para type-safe APIs

inline fun <reified T> Gson.fromJson(json: String): T =
    fromJson(json, T::class.java)

data class Usuario(val nombre: String, val email: String)
val usuario: Usuario = gson.fromJson(json)

4. runCatching para operaciones que pueden fallar

fun dividir(a: Int, b: Int): Result<Int> = runCatching { a / b }

dividir(10, 2)
    .onSuccess { println("Resultado: $it") }
    .onFailure { println("Error: ${it.message}") }

5. Sequence para lazy processing

val lista = (1..1_000_000).toList()

val lazy = lista.asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .toList()

6. Extension functions para logging

fun Any.logInfo(mensaje: String) {
    println("[${this::class.simpleName}] $mensaje")
}

7. require() y check() para precondiciones

data class Persona(val nombre: String, val edad: Int) {
    init {
        require(nombre.isNotBlank()) { "El nombre no puede estar vacío" }
        require(edad >= 0) { "La edad no puede ser negativa" }
    }
}

8. takeIf y takeUnless para filtros inline

val numero = 42
val par = numero.takeIf { it % 2 == 0 }
val impar = numero.takeUnless { it % 2 == 0 }

9. Comandos esenciales

gradle build       # Compila + tests
gradle test        # Solo tests
gradle koverReport # Cobertura

10. Type aliases para tipos complejos

typealias JsonMap = Map<String, Any>
typealias Callback<T> = (Result<T>) -> Unit
typealias IdUsuario = Long

Fuente: Kotlin docs — Idioms, Kotlin docs — Coding Conventions


⚠️ Errores comunes

Si algo falla, lo más probable es que esté en esta tabla:

ErrorCausaSolución
Unresolved reference: XVariable, función o clase no declarada, o falta un importAñade el import necesario; revisa la ortografía y el scope
Type mismatch: inferred type is X but Y was expectedPasas un tipo a un parámetro o variable de tipo incompatibleUsa .toX() de conversión, o ajusta el tipo declarado; revisa nullability (X?)
Smart cast to X is impossible, because 'Y' is a mutable propertyIntentas smart cast en una propiedad var que puede cambiar entre comprobación y usoUsa una variable local val, sincroniza con synchronized, o aplica as explícito
Unresolved reference: printlnFalta el import kotlin.io.* o estás en un contexto que no lo incluye automáticamenteAñade import kotlin.io.println o usa println() que está en el paquete raíz de Kotlin
Could not resolve all files for configuration ':classpath'Gradle no puede resolver las dependencias del plugin (repositorio mal configurado o sin red)Verifica repositories { mavenCentral(); google() } en el bloque buildscript
Unresolved reference to gradleEl wrapper gradlew no existe o el proyecto no fue sincronizadoEjecuta gradle wrapper o sincroniza el proyecto en IntelliJ/Android Studio
'gradle' is not recognized as an internal or external commandGradle no está instalado o no está en el PATHInstala Gradle desde gradle.org o usa el wrapper ./gradlew

🔗 Guías relacionadas

Referencias y documentación oficial

Fuentes de esta guía

Esta guía se ha elaborado a partir de las siguientes fuentes consultadas durante su desarrollo:


🏆 Retos Relacionados

Pon a prueba lo aprendido con estos desafíos:

COMPARTIR:
COMENTARIOS:

📋 Contenido