Índice del curso

Kotlin desde cero hasta experto

Un tutorial secuencial y profundo del lenguaje Kotlin: instalación en Windows y Linux, fundamentos, POO moderna, null safety, corutinas y Flow. Todo lo necesario para llegar a Android dominando el lenguaje oficial.

38 capítulos Bootstrap 5.3 Modo claro / oscuro Optimizado para móvil Kotlin 2.x
38
Capítulos
150+
Ejemplos de código
3
Niveles: básico a experto
0
Requisitos previos
Cómo usar este tutorial: sigue los capítulos en orden (el índice está en el menú si lees desde el móvil). Cada capítulo tiene objetivos, teoría, ejemplos ejecutables y puntos clave al final. Escribe y ejecuta cada ejemplo: la práctica es la única vía para dominar un lenguaje.

1 · ¿Qué es Kotlin?

Básico ~10 min

Kotlin es un lenguaje creado por JetBrains (los de IntelliJ IDEA), anunciado en 2011 y estable desde su versión 1.0 en 2016. En Google I/O 2017 Google anunció soporte oficial para Android y en 2019 lo declaró lenguaje preferente del sistema. Hoy también sirve para servidores, web, escritorio y apps multiplataforma con Kotlin Multiplatform.

¿Por qué aprender Kotlin?

  • Es el lenguaje oficial de Android: Jetpack Compose, toda la documentación moderna y los codelabs están pensados en Kotlin.
  • 100 % compatible con Java: puedes usar cualquier librería Java existente y mezclar código de ambos.
  • Multiparadigma y moderno: POO sólida + programación funcional + null safety desde el diseño + corutinas para concurrencia.
  • Conciso pero legible: elimina el boilerplate de Java sin volverse críptico como otros lenguajes.

Características clave

Null Safety nativo: el compilador distingue tipos anulables y no anulables desde el día uno.
Sobre JVM: hereda un ecosistema gigante (librerías, herramientas, rendimiento maduro).
Corutinas: concurrencia ligera y legible con async/await al estilo Kotlin.
Extensiones y data/sealed classes: herramientas que eliminan clases utilitarias y estados imposibles.
Jetpack Compose: la UI moderna de Android se declara con funciones Kotlin.
Multiplataforma real: JVM, JS/WASM, iOS, Linux, macOS y Windows con Kotlin Multiplatform.

Kotlin frente a otros lenguajes

LenguajeRelación con Kotlin
JavaKotlin compila al mismo bytecode y convive con él; casi siempre «Java menos ruido» con extras.
ScalaSimilar potencia funcional, con curva mucho más amable y compilación rápida.
SwiftFilosofía gemela (null safety, protocolos/extensión, optionals); Kotlin corre donde Swift no llega.
DartAmbos modernos y null-safe; Kotlin aporta el ecosistema JVM y corutinas, Dart brilla con Flutter.

Un vistazo rápido al lenguaje

// Todo esto se explica capítulo por capítulo. Por ahora, disfruta la lectura.
data class Usuario(val nombre: String, val edad: Int) {
    val esMayorDeEdad get() = edad >= 18
}

fun main() {
    val usuarios = listOf(
        Usuario("Ana", 34),
        Usuario("Luis", 15),
    )

    usuarios
        .filter { it.esMayorDeEdad }
        .forEach { println("Hola $it") }
}
Hola Usuario(nombre=Ana, edad=34)
Prueba sin instalar nada: en Kotlin Playground escribes y ejecutas Kotlin en el navegador. Ideal hasta el capítulo 3.

Puntos clave

  • Kotlin es oficial en Android y corre sobre JVM con interop total con Java.
  • Null safety, concisión y corutinas son sus tres señas de identidad.
  • Sirve hoy para móvil, servidor, web, escritorio y multiplataforma (KMP).

2 · Instalación paso a paso (Windows y Linux)

Básico ~15 min

Necesitas dos piezas: un JDK (Kotlin compila a bytecode JVM) y el compilador kotlinc. Además te presentamos IntelliJ IDEA, el editor que usarás el resto de tu vida Kotlin.

Atajo válido: si instalas IntelliJ IDEA Community, él mismo puede descargar JDK y Kotlin por ti (lo verás al final del capítulo). Los pasos manuales que siguen te sirven para entender qué hay debajo y para servidores/CI.

2.1 · Paso previo universal: instalar un JDK

Cualquier JDK 17 o superior vale. Recomendado: Eclipse Temurin (gratuito, mantenido).

# Windows (PowerShell como administrador): winget install EclipseAdoptium.Temurin.21.JDK # Ubuntu/Debian: sudo apt update && sudo apt install -y openjdk-21-jdk # Verificar en ambos: java -version

2.2 · Compilador Kotlin en Linux (SDKMAN!, la vía cómoda)

curl -s "https://get.sdkman.io" | bash source "$HOME/.sdkman/bin/sdkman-init.sh" sdk install kotlin kotlinc -version
info: kotlinc-jvm 2.x.x (JRE 21.x)

SDKMAN! también gestiona versiones de Java (sdk install java 21-tem) y actualiza con sdk upgrade kotlin.

2.3 · Compilador Kotlin en Windows

choco install kotlinc # PowerShell admin scoop install kotlin # alternativa sin admin kotlinc -version

  1. Descarga kotlin-compiler-x.y.z.zip desde GitHub Releases.
  2. Descomprime en C:\kotlinc.
  3. Añade C:\kotlinc\kotlinc\bin a la variable Path de tu usuario (menú inicio → «variables de entorno»).
  4. Abre una terminal nueva y ejecuta kotlinc -version.

2.4 · Editor: IntelliJ IDEA Community

  1. Descarga IntelliJ IDEA Community Edition (gratis) desde jetbrains.com/idea.
  2. Al crear tu primer proyecto Kotlin, el asistente puede descargar el JDK solo («Download JDK», Temurin 21).
  3. Trae autocompletado inteligente, refactorizaciones, depurador, inspecciones y formateo. Es el estándar del mundo Kotlin.
  4. Alternativa ligera: VS Code + extensión «Kotlin» (funciona para leer este curso; para proyectos reales gana IDEA).

Puntos clave

  • JDK 17+ obligatorio + kotlinc; verifica ambos antes de seguir.
  • En Linux, SDKMAN! instala y actualiza todo con dos comandos.
  • IntelliJ IDEA Community es el entorno recomendado del curso.

3 · Tu primer programa y el entorno

Básico ~15 min

3.1 · El clásico «Hola, mundo»

Crea un archivo hola.kt:

fun main() {
    println("Hola, mundo")
}

Fíjate en dos libertades que Java no te da: no hace falta ninguna clase y no hace falta punto y coma. La ejecución empieza siempre en main().

3.2 · Tres formas de ejecutarlo

kotlinc hola.kt -include-runtime -d hola.jar java -jar hola.jar
Hola, mundo

El flag -include-runtime mete las librerías Kotlin dentro del jar para poder repartirlo sin dependencias.

Un archivo prueba.kts se ejecuta directo, sin función main ni compilación manual — perfecto para probar ideas:

# prueba.kts contiene solo: println("Ejecutado como script") kotlinc -script prueba.kts

  1. New Project → Kotlin → JVM → marca «Gradle» (o «IntelliJ» para empezar simple).
  2. Crea Main.kt, escribe main y pulsa Tab: el IDE genera la función por ti.
  3. Pulsa el triángulo verde junto a fun main()Run.

3.3 · Estructura de un proyecto con Gradle

mi-app/ ├── build.gradle.kts # configuración y dependencias (capítulo 33) ├── settings.gradle.kts # nombre del proyecto └── src/ ├── main/kotlin/ # tu aplicación └── test/kotlin/ # pruebas unitarias (capítulo 35)

3.4 · Tu primer cambio de verdad

fun main() {
    val saludo = "Kotlin"
    val anio = 2026
    println("Aprendiendo $saludo en el año $anio")

    val args: Array<String> = arrayOf("Ana", "Luis") // lo que llegaría por línea de comandos
    println("Argumentos recibidos: ${args.size}")
}
Aprendiendo Kotlin en el año 2026 Argumentos recibidos: 2
  • val declara un valor inmutable (lo normal en Kotlin).
  • $saludo inserta variables dentro del texto; ${expresión} evalúa código.
  • Para recibir argumentos reales: fun main(args: Array<String>).
REPL incluido: ejecuta kotlinc sin argumentos y obtienes una consola interactiva para probar expresiones al vuelo. En IDEA: Tools → Kotlin → Kotlin REPL.

Puntos clave

  • fun main() sin clases ni punto y coma.
  • Tres caminos: jar compilado, scripts .kts o el botón Run de IDEA.
  • Proyecto estándar: Gradle con src/main/kotlin y src/test/kotlin.

4 · Sintaxis básica: reglas del juego

Básico ~12 min

Kotlin es un lenguaje de llaves y sensible a saltos de línea: el estilo recuerda a Java/C, pero con reglas pensadas para eliminar ruido.

Las cinco reglas que debes memorizar

// 1) Punto y coma OPCIONAL: solo si pones dos sentencias en una línea
val a = 1; val b = 2

// 2) Bloques con llaves; la indentación es convención (4 espacios)
fun saluda() {
    println("Hola")
}

// 3) Comentarios idénticos a Java/Dart
// una línea
/* un bloque
   multilínea */

// 4) Los nombres usan lowerCamelCase para variables y funciones
val precioFinal = 99.9
fun calculaTotal() {}

// 5) Las clases van en UpperCamelCase y los archivos pueden llamarse como sea
class Factura {}

Sensible a saltos de línea

El compilador asume fin de sentencia al terminar la línea. Si partes una expresión, hazlo después de un operador o abre la línea con paréntesis:

val total = 100 +
    200            // OK: la línea termina en '+'

val lista = listOf(
    "a", "b",      // OK: dentro de paréntesis da igual
)

Palabras clave «contextuales»

Kotlin tiene palabras reservadas (fun, val, var, class, when…) pero también permite usar casi cualquier identificador escapándolo entre tildes inversas — útil al interactuar con APIs Java:

val `class` = "mala idea en general, pero legal"
val `in` = 42      // necesario a veces con librerías Java

Puntos clave

  • Sin punto y coma, sin clases obligatorias, sin static.
  • Bloques con llaves; partir líneas tras operadores.
  • Nombres: lowerCamelCase para miembros, UpperCamelCase para tipos.

5 · Variables: val frente a var

Básico ~15 min

Kotlin no tiene «variables mutables por defecto»: tienes que decidirlo tú. Es una decisión consciente que el compilador aprovecha.

Palabra claveMutabilidadCuándo usarla
valSolo lectura (referencia inmutable)Por defecto. El 90–95 % de tu código
varLectura/escritura (mutable)Solo cuando el valor cambia con el tiempo (contadores, acumuladores)
const valConstante de compilaciónSolo nivel top-level u objeto, tipos primitivos y String
const val IVA = 0.21              // constante global de compilación

fun main() {
    val nombre = "Ana"            // inferido: String
    var saldo = 100               // inferido: Int

    saldo += 50                   // OK: es var
    // nombre = "Otra"             // ERROR de compilación: val no reasignable

    // Tipado explícito opcional:
    val activo: Boolean = true
    var temperatura: Double = 21.5
}

«Inmutable» significa referencia, no contenido

val numeros = mutableListOf(1, 2, 3)

numeros.add(4)        // OK: el CONTENIDO puede cambiar
// numeros = mutableListOf()   // ERROR: la REFERENCIA no

Declaración tardía: lateinit y lazy

class Servicio {
    lateinit var cliente: Cliente     // prometes inicializarla antes de usarla

    val cache: Map<String, Int> by lazy {   // se calcula SOLO la primera vez que se lee
        cargarDatosCostosos()
    }
}
  • lateinit: solo var, solo tipos no primitivos; lanza excepción si aún no está lista.
  • by lazy: sobre val; thread-safe por defecto e ideal para valores costosos.
Regla de oro del curso: empieza siempre con val. Cambia a var únicamente cuando el compilador te obligue. Tu código futuro (y tus compañeros) lo agradecerán.

Puntos clave

  • val por defecto, var justificado, const val para constantes globales.
  • La inferencia de tipos evita anotarlos en el 95 % de los casos.
  • lateinit y by lazy cubren la inicialización diferida.

6 · Números, caracteres y booleanos

Básico ~15 min

A diferencia de Java, en Kotlin todo son objetos: no hay tipos primitivos «especiales» para ti (el compilador los optimiza por debajo). Pero sí tienen tamaños distintos:

TipoTamañoRango / uso típico
Byte8 bits-128 a 127
Short16 bits-32.768 a 32.767
Int32 bitsEl entero por defecto (±2,1 mil millones)
Long64 bitsEnteros grandes; literal con L: 3_000_000_000L
Float32 bitsDecimal de precisión simple; literal con F
Double64 bitsEl decimal por defecto. Precisión doble
val entero = 42                 // Int
val grande = 9_000_000_000L     // Long (los _ son solo legibilidad)
val decimal = 3.14159           // Double
val flotante = 2.5F             // Float
val hexa = 0xFF                 // literales hexadecimales y binarios:
val binario = 0b1010            //   255 y 10 respectivamente

println(entero + decimal)       // mezcla automática -> Double: 45.14159

¡Ojo! No hay conversión implícita

Kotlin exige conversiones explícitas entre tipos numéricos. Evita sorpresas silenciosas:

val i: Int = 10
// val l: Long = i        // ERROR de compilación
val l: Long = i.toLong()  // OK
val d = l.toDouble()

println(7 / 2)             // 3    ¡división ENTERA trunca!
println(7.0 / 2)           // 3.5
println(-7.floorDiv(2))    // 4    división hacia abajo
println(-7.mod(3))         // 2    resto siempre positivo

Char: un carácter Unicode

val letra: Char = 'A'         // ¡comillas SIMPLES!
println(letra.code)           // 65  código Unicode
println('a'.uppercase())      // A
println('5'.isDigit())        // true
for (c in 'a'..'e') print(c)  // abcde  (los Char también forman rangos)

Boolean y la evaluación en cortocircuito

val conectado = true
val esAdmin = false

if (conectado && !esAdmin) println("Acceso limitado")

// && deja de evaluar si el primero es false:
val nombre: String? = null
if (nombre != null && nombre.length > 0) { /* seguro */ }

Puntos clave

  • Int y Double cubren el 95 % de casos; Long para IDs grandes.
  • Conversiones SIEMPRE explícitas (toLong(), toDouble()…).
  • División entera trunca; usa floorDiv/mod para semántica matemática.

7 · Cadenas de texto (String) a fondo

Básico ~20 min

Los strings en Kotlin son inmutables, indexables como arrays y —la joya— admiten plantillas integradas en el lenguaje.

Plantillas: $variable y ${expresión}

val nombre = "Ana"
val edad = 34

println("Hola, $nombre")                    // Hola, Ana
println("El año que viene cumplirás ${edad + 1}")  // expresiones completas
println("Objetos también: ${listOf(1, 2)}")       // [1, 2]

Operaciones básicas

val s = "Kotlin es genial"

s.length               // 16
s[0]                   // 'K'   índice como corchetes
s.uppercase()          // KOTLIN ES GENIAL
s.replace("genial", "increíble")
s.substring(0, 6)      // Kotlin
s.split(" ")           // [Kotlin, es, genial]
s.trim()               // quita espacios en extremos
"ab".repeat(3)         // ababab
"-".padEnd(20, '=')    // -------------------  útil para menús
s.contains("es")       // true
"2026-08-22".startsWith("2026")  // true

Comparación: == compara contenido

Otra mejora sobre Java: == ya no compara referencias, sino valores (llama a equals). Para identidad existe ===:

val a = "kotlin"
val b = "KO".lowercase() + "TLIN".lowercase()

println(a == b)    // true   ¡contenido!
println(a === b)   // false  (aquí son objetos distintos)

Raw strings: cadenas multilínea sin escapes

val json = """
    {
        "usuario": "$nombre",
        "activo": true
    }
""".trimIndent()

println(json)
// {
//     "usuario": "Ana",
//     "activo": true
// }

val ruta = """C:\Users\ana\documentos"""   // sin escapar las contrabarras

trimIndent() elimina la sangría común; con trimMargin("|") marcas tú qué líneas recortar.

Conversión número ↔ texto (¡segura!)

val n = "123".toInt()          // 123
val x = "abc".toIntOrNull()    // null: NO explota, devuelve nulo (capítulo 16)

println("%.2f".format(3.14159))   // 3.14   formato tipo printf
toInt() lanza excepción si el texto no es un número válido. En entrada de usuario o APIs prefiere siempre toIntOrNull().

Puntos clave

  • Las plantillas $var / ${expr} hacen innecesario concatenar con +.
  • Raw strings """...""" para JSON, SQL, HTML y rutas Windows.
  • == ya compara contenido; convierte números con variantes *OrNull.

8 · Operadores y rangos

Básico ~15 min

Operadores clásicos

val a = 7; val b = 2

a + b    // 9     también concatena Strings y colecciones
a - b    // 5
a * b    // 14
a / b    // 3     ¡entero! (capítulo 6)
a % b    // 1

// Comparación estilo matemático:
println(3 < 5)          // true
println("a".."z")       // los rangos hacen el resto de la magia ↓

Rangos: la sintaxis que enamora

Kotlin convierte los intervalos en ciudadanos de primera clase con el operador ..:

// Creación
val unoACinco = 1..5            // 1, 2, 3, 4, 5   (¡incluye el final!)
val exclusivo  = 1 until 5      // 1, 2, 3, 4      (excluye el final)
val descendente = 5 downTo 1    // 5, 4, 3, 2, 1
const val paso = 2
val pares = 0..10 step paso     // 0, 2, 4, 6, 8, 10
val letras = 'a'..'e'

// Pertenencia con 'in'
if (edad in 18..65) println("Edad laboral")
if ('x' !in letras) println("fuera del abecedario inicial")

// Iteración (capítulo 10 lo amplía)
for (i in 1..3) print(i)        // 123

Operadores lógicos y de identidad

OperadorSignificado
&&, ||, !Y, O, NO lógicos (con cortocircuito)
== / !=Igualdad de contenido (llama a equals)
=== / !==Igualdad de referencia (mismo objeto en memoria)
in / !inPertenece a colección o rango
is / !isComprobación de tipo (con smart cast)
fun describe(x: Any): String =
    if (x is Int && x in 1..100) "entero pequeño"
    else if (x is String) "texto de ${x.length} letras"
    else "otra cosa"

println(describe(42))        // entero pequeño
println(describe("Kotlin"))  // texto de 6 letras
Fíjate: tras x is String, dentro del bloque puedes usar x.length directamente. El compilador «recuerda» el tipo — eso es un smart cast, y lo retomaremos en los capítulos 16 y 23.

Puntos clave

  • .. crea rangos inclusivos; until, downTo y step los afinan.
  • in sirve para pertenencia e iteración.
  • is activa smart casts sin conversiones manuales.

9 · Condicionales: if y when

Básico ~15 min

if como expresión

En Kotlin if devuelve valor. Adiós al operador ternario:

val edad = 20

// Sentencia normal:
if (edad >= 18) println("mayor de edad") else println("menor")

// EXPRESIÓN (el else es obligatorio si se asigna):
val categoria = if (edad < 13) "niño"
                else if (edad < 18) "adolescente"
                else "adulto"

when: el switch mejorado

when compara contra constantes, rangos, tipos, expresiones… y tampoco necesita break:

fun clasifica(n: Int): String = when {
    n == 0 -> "cero"
    n in 1..9 -> "un dígito"
    n % 2 == 0 -> "par grande"
    else -> "impar grande"
}

// Con sujeto:
fun diaTipo(dia: String) = when (dia) {
    "sábado", "domingo" -> "fin de semana"
    "lunes" -> "principio duro"
    else -> "entre semana"
}
clasifica(42) → "par grande" diaTipo("domingo") → "fin de semana"
  • Sin sujeto (when { ... }) actúa como una cascada de if/else potente.
  • Con sujeto, cada rama puede ser valor, lista de valores, rango o tipo (is String ->).
  • El else es obligatorio cuando el resultado se asigna o la función devuelve.
  • A partir de Kotlin 1.7+, si olvidas cubrir un caso el compilador te avisa (¡y con sealed classes lo exige!, capítulo 23).

Combinando ambos

val nota = 87
val mencion = when {
    nota >= 90 -> "Sobresaliente"
    nota >= 70 -> "Notable"
    nota >= 60 -> "Aprobado"
    else -> "Suspenso"
}
println(mencion)   // Notable

Puntos clave

  • No existe el operador ternario: usa if como expresión.
  • when sustituye switch y cadenas largas de if/else con más legibilidad.
  • Ramas por valor, lista, rango, tipo o condición libre.

10 · Bucles: for, while y progresiones

Básico ~15 min

for: siempre sobre algo iterable

// Rangos (capítulo 8)
for (i in 1..5) print("$i ")          // 1 2 3 4 5
println()

// Colecciones
val frutas = listOf("pera", "kiwi", "uva")
for (fruta in frutas) print("$fruta ") // pera kiwi uva
println()

// Con índice y valor a la vez:
for ((i, fruta) in frutas.withIndex()) println("$i: $fruta")

// Sobre un String:
for (c in "Kt") print(c + "-")         // K-t-

while y do/while

var intento = 1
while (intento <= 3) {
    println("Intento $intento")
    intento++
}

// do/while garantiza al menos una pasada:
do {
    println("se ejecuta una vez como mínimo")
    intento++
} while (false)

Control de flujo: break, continue y etiquetas

for (i in 1..5) {
    if (i == 3) continue     // salta el resto de ESTA iteración
    if (i == 5) break        // abandona el bucle entero
    print(i)                 // 124
}
println()

// Etiquetas para salir de bucles anidados de golpe:
busqueda@ for (fila in 1..3) {
    for (col in 1..3) {
        if (fila * col == 6) break@busqueda   // sale de AMBOS bucles
    }
}

El giro idiomático Kotlin: funciones sobre colecciones

Pronto descubrirás que muchos bucles clásicos se escriben mejor con forEach, map o filter (capítulos 13–14). Compáralos:

val nums = listOf(1, 2, 3, 4)

// Estilo imperativo:
var suma = 0
for (n in nums) if (n % 2 == 0) suma += n

// Estilo funcional (el que usarás a diario):
val sumaFp = nums.filter { it % 2 == 0 }.sum()
println(sumaFp)   // 6 en ambos casos

Puntos clave

  • for itera rangos, colecciones, strings… sin contadores manuales.
  • withIndex(), break/continue con etiquetas cubren los casos complejos.
  • Prefiere operaciones funcionales cuando puedas; guarda while para condiciones dinámicas.

11 · Funciones I: los fundamentos

Básico ~15 min

Se declaran con fun. Pueden vivir a nivel de archivo (¡sin clases contenedoras!), dentro de clases o incluso dentro de otras funciones.

// Sintaxis completa:  fun nombre(param: Tipo): TipoRetorno { ... }
fun suma(a: Int, b: Int): Int {
    return a + b
}

// Si el cuerpo es UNA expresión: llaves fuera y tipo inferido
fun multiplica(a: Int, b: Int) = a * b

fun saluda(nombre: String): Unit {   // Unit = "no devuelve nada", se omite
    println("Hola $nombre")
}

println(suma(2, 3))        // 5
println(multiplica(4, 5))  // 20

Argumentos nombrados

Puedes llamar indicando el nombre de cada parámetro — orden libre y autodocumentación:

fun envia(destinatario: String, asunto: String, urgente: Boolean = false) {
    println("[$asunto] -> $destinatario ${if (urgente) "(URGENTE)" else ""}")
}

envia("ana@x.com", "Reunión")                       // usa el default
envia(asunto = "Fire!", destinatario = "soporte", urgente = true)

Valores por defecto: adiós sobrecargas en cascada

fun conecta(host: String, puerto: Int = 8080, ssl: Boolean = true) =
    println("$host:$puerto ${if (ssl) "https" else "http"}")

conecta("api.ejemplo.com")                 // https://api.ejemplo.com:8080
conecta("local", 3000, false)              // http://local:3000

Funciones locales y top-level

fun procesaPedido(total: Double): Double {
    // función DENTRO de otra: ve sus parámetros, no contamina la clase
    fun aplicaIva(x: Double) = x * 1.21

    val conIva = aplicaIva(total)
    return if (conIva > 100) conIva * 0.95 else conIva   // descuento grande
}

println(procesaPedido(200.0))   // 230.0 * 0.95 = 218.5

Puntos clave

  • fun, tipos obligatorios en parámetros; retorno inferible si el cuerpo es expresión.
  • Argumentos nombrados + valores por defecto eliminan la mayoría de sobrecargas.
  • Funciones locales mantienen helpers cerca de su único uso.

12 · Funciones II: parámetros avanzados

Intermedio ~15 min

vararg: número variable de argumentos

fun maximo(vararg numeros: Int): Int {
    var mejor = Int.MIN_VALUE
    for (n in numeros) if (n > mejor) mejor = n
    return mejor
}

println(maximo(3, 9, 4))       // 9
println(maximo())              // -2147483648 (caso vacío: ¡documéntalo!)

val datos = intArrayOf(5, 7, 6)
println(maximo(*datos))        // el operador * «expande» el array

Orden recomendado de parámetros

fun ejemplo(obligatorios..., vararg..., conDefault...) # patrón habitual fun reporte(titulo: String, vararg lineas: String, formato: String = "txt")

Con argumentos nombrados puedes saltarte el orden: reporte("Resumen", formato = "csv", lineas...) … aunque lo claro gana siempre.

Sobrecarga (overload) cuando toca

fun area(lado: Double) = lado * lado
fun area(base: Double, altura: Double) = base * altura

println(area(2.0))          // 4.0
println(area(2.0, 3.0))     // 6.0

Funciones infix y operadores propios

class Potencia(val base: Int) {
    infix fun elevadoA(exponente: Int) = base.toDouble().pow(exponente)
}
// infix permite llamar sin punto ni paréntesis:
val p = Potencia(2) elevadoA 8      // 256.0

// Sobrecargar operadores aritméticos para TU tipo:
data class Vector(val x: Int, val y: Int) {
    operator fun plus(otro: Vector) = Vector(x + otro.x, y + otro.y)
}
println(Vector(1, 2) + Vector(3, 4))   // Vector(x=4, y=6)
Moderación: infix y operators son azúcar potente. Resérvalos para dominios matemáticos o DSLs claros; un + que borra archivos es mala idea.

Puntos clave

  • vararg + expansión con *.
  • Sobrecarga solo si los tipos realmente cambian el significado.
  • infix y operator permiten APIs casi naturales.

13 · Lambdas y funciones como valores

Intermedio ~20 min

Una lambda es una función sin nombre que se guarda en una variable o se pasa como argumento. Es la pieza que hace brillar a las colecciones y a Jetpack Compose.

Sintaxis básica

// Tipo: (parámetros) -> retorno
val cuadrado: (Int) -> Int = { n -> n * n }

// Con tipos inferidos:
val cubo = { n: Int -> n * n * n }

println(cuadrado(5))   // 25
println(cubo(3))       // 27

// Parámetro único implícito: it
val doble: (Int) -> Int = { it * 2 }
println(doble(21))     // 42

// Sin parámetros ni retorno útil:
val ping = { println("ping") }

Funciones que reciben funciones

fun opera(a: Int, b: Int, operacion: (Int, Int) -> Int): Int =
    operacion(a, b)

println(opera(6, 2) { x, y -> x + y })   // 8
println(opera(6, 2) { x, y -> x - y })   // 4

// Si el ÚLTIMO parámetro es una lambda, sale fuera del paréntesis
// (trailing lambda): la sintaxis más usada en Kotlin real.

Referencias a funciones con ::

fun esPar(n: Int) = n % 2 == 0

val nums = listOf(1, 2, 3, 4)
println(nums.filter(::esPar))    // [2, 4]
println(nums.map(Int::toString)) // [1, 2, 3, 4]  referencia de clase

Closures: las lambdas recuerdan su entorno

fun contador(): () -> Int {
    var cuenta = 0
    return { ++cuenta }          // captura `cuenta` aunque la función ya terminó
}

val siguiente = contador()
println(siguiente())   // 1
println(siguiente())   // 2
println(siguiente())   // 3

inline: lambdas sin coste oculto

Cada lambda puede crear un objeto. Las funciones estándar de Kotlin están marcadas inline: el compilador «pega» el cuerpo y la lambda directamente, eliminando ese coste. Para tus propias APIs de alto rendimiento:

inline fun cronometra(bloque: () -> Unit) {
    val inicio = System.nanoTime()
    bloque()
    println("tardó ${(System.nanoTime() - inicio) / 1000} µs")
}

Puntos clave

  • Sintaxis { param -> cuerpo }; it cuando hay un solo parámetro.
  • Trailing lambda: el último argumento sale del paréntesis.
  • ::funcion convierte funciones existentes en valores.
  • inline elimina el overhead de objetos lambda.

14 · Colecciones I: List e Iterable

Intermedio ~20 min

Kotlin separa en el sistema de tipos lo que otros lenguajes dejan al honor: cada colección tiene versión solo lectura y versión mutable.

Solo lecturaMutableQué es
List<T>MutableList<T>Secuencia ordenada con índice (la estrella)
Set<T>MutableSet<T>Sin duplicados
Map<K,V>MutableMap<K,V>Pares clave-valor
// Creación
val soloLectura = listOf("a", "b")           // List<String>
val mutable      = mutableListOf(1, 2, 3)    // MutableList<Int>

// Acceso
val frutas = listOf("pera", "kiwi", "uva")
frutas[0]         // pera
frutas.first()    // pera
frutas.last()     // uva
frutas.getOrNull(9)   // null: sin excepción (capítulo 16)
frutas.size       // 3

// Mutaciones (solo en MutableList):
mutable.add(4)
mutable.removeAt(0)
mutable += listOf(5, 6)     // azúcar para addAll
mutable[0] = 99

// frutas.add(...)   // ERROR: List no tiene add. El compilador te protege.

El pipeline funcional (lo usarás todos los días)

data class Producto(val nombre: String, val precio: Double, val stock: Int)

val catalogo = listOf(
    Producto("Teclado", 45.0, 12),
    Producto("Ratón", 25.5, 0),
    Producto("Monitor", 199.0, 3),
)

catalogo.filter { it.stock > 0 }              // quédate con los que cumplen
        .sortedByDescending { it.precio }     // ordena descendente
        .map { it.nombre }                    // transforma a otro tipo
        .forEach(::println)                   // Monitor, Teclado

Reducciones y búsquedas rápidas

val precios = listOf(45.0, 25.5, 199.0)

precios.sum()                       // 269.5
precios.maxOrNull()                 // 199.0
precios.average()                   // 89.83...
precios.fold(100.0) { acc, p -> acc + p }   // 369.5: acumulador inicial

catalogo.find { it.nombre == "Ratón" }      // primer que cumpla o null
catalogo.any { it.stock == 0 }              // true: ¿hay alguno?
catalogo.all { it.precio > 20 }             // true: ¿cumplen todos?
catalogo.count { it.stock > 0 }             // 2
catalogo.groupBy { it.stock > 0 }           // Map>
filter/map devuelven listas nuevas — encadenar muchas puede crear intermedios innecesarios. Para pipelines enormes usa asSequence() (evaluación perezosa, como Streams de Java).

Puntos clave

  • Devuelve siempre tipos de solo lectura; mutabilidad explícita y localizada.
  • filter → sortedBy → map → forEach: el pipeline canónico.
  • find/any/all/count/groupBy/fold resuelven consultas sin bucles manuales.

15 · Colecciones II: Set y Map

Intermedio ~20 min

Set: elementos únicos

val vocales = setOf('a', 'e', 'i')            // LinkedHashSet por debajo
val mutables = mutableSetOf("rojo", "verde")

mutables.add("azul")
mutables.add("azul")          // duplicado: se ignora silenciosamente
println(mutables.size)        // 3

// Operaciones de conjuntos:
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)

a union b             // [1, 2, 3, 4, 5]
a intersect b         // [3]
a subtract b          // [1, 2]

val visitados = mutableSetOf()
if ("pagina2" in visitados) println("ya la viste")   // pertenencia O(1)

Map: diccionarios clave-valor

// Creación con pares o con el DSL «to»:
val capitales = mapOf(
    "España" to "Madrid",
    "Francia" to "París",
)

val stock = mutableMapOf(
    "teclado" to 12,
    "ratón" to 30,
)

// Lectura
capitales["España"]              // Madrid
capitales["Italia"]              // null (¡no excepción!)
capitales.getOrDefault("Italia", "?")   // ?
capitales.containsKey("Francia") // true
capitales.keys; capitales.values

// Escritura
stock["monitor"] = 5             // insertar o actualizar
stock.putIfAbsent("webcam", 7)
stock.merge("teclado", -1, Int::plus)   // 11: suma sobre el valor actual

Iteración y transformación de Maps

for ((pais, capital) in capitales) {
    println("$capital es la capital de $pais")
}

val precios = mapOf("café" to 1.5, "té" to 1.2)
precios.mapValues { (_, precio) -> precio * 1.21 }   // aplica IVA
precios.filterKeys { it == "té" }

// Agrupar una lista EN un map (patrón muy común en apps reales):
data class Persona(val nombre: String, val ciudad: String)
val gente = listOf(
    Persona("Ana", "Madrid"), Persona("Luis", "Barcelona"),
    Persona("Eva", "Madrid"),
)
println(gente.groupBy { it.ciudad })
// {Madrid=[Ana, Eva], Barcelona=[Luis]}
En Kotlin moderno casi nunca construyes Maps vacíos y los rellenas a mano: groupBy, associateBy y associateWith los generan desde listas.

Puntos clave

  • Set: unicidad + pertenencia O(1); operaciones union/intersect/subtract.
  • map[clave] devuelve nulo si no existe — prepárate para el capítulo siguiente.
  • groupBy/associate* convierten colecciones en índices sin bucles.

16 · Null Safety: el sistema anti-nulos

Intermedio ~25 min

La NullPointerException se ganó el apodo de «el error del billón de dólares». Kotlin la combate en tiempo de compilación: el tipo te dice si un valor puede ser nulo o no.

String y String? son tipos distintos

var nombre: String = "Ana"
// nombre = null        // ERROR: String NO admite nulos

var apodo: String? = null     // el interrogante lo permite
apodo = "anita"

val longitud1 = nombre.length      // OK garantizado
// val longitud2 = apodo.length    // ERROR: podría ser null

?. — llamada segura

val apodo: String? = null

println(apodo?.length)    // null   (no explota: propaga el nulo)
// Encadenable:
// pedido?.cliente?.direccion?.ciudad

?: — operador Elvis

val apodo: String? = null

val mostrado = apodo ?: "anónimo"       // si es null, usa el valor alternativo
println(mostrado)                        // anónimo

// Combinado (patrón diario):
val largo = (apodo ?: "").length         // 0

// También puede lanzar o retornar:
fun saluda(usuario: String?) =
    println("Hola ${usuario ?: return}")  // sale si no hay usuario

Smart cast: comprueba y usa sin convertir

fun imprimeEnMayusculas(texto: String?) {
    if (texto != null) {
        println(texto.uppercase())   // ¡sin ! ni conversión!
    }                                // dentro del if el compilador SABE que no es null
}

// Con retorno anticipado queda aún más limpio:
fun procesa(datos: String?): Int {
    val d = datos ?: return 0        // guard clause idiomática
    return d.length                  // d ya es String normal
}

let: ejecuta solo si hay valor

val email: String? = obtenerEmail()

email?.let { enviarBienvenida(it) }      // solo se ejecuta si no es null
// equivalente legible a:
if (email != null) { enviarBienvenida(email) }

!! — la afirmación peligrosa

val configuracion: String? = cargar()

val c = configuracion!!    // "confío en que no es null"
                           // si te equivocas: KotlinNullPointerException EN TIEMPO DE EJECUCIÓN
Regla del curso: !! está prohibido salvo en tests. Si sientes la necesidad de usarlo, casi siempre falta un Elvis, una guard clause o rediseñar el tipo.

Herramientas de la librería estándar

fun registra(id: Int) {
    val usuario = buscar(id)
    // lanza IllegalStateException con mensaje claro si viene null:
    val u = requireNotNull(usuario) { "El usuario $id debe existir" }
    println(u.nombre)
}
// primos útiles: checkNotNull() para invariantes, error("mensaje") para fallos deliberados

Puntos clave

  • Tipo? declara anulabilidad; el compilador obliga a tratarla.
  • Caja de herramientas: ?., ?:, smart casts y ?.let{}.
  • !! solo en pruebas; en producción, Elvis + guards.

17 · Clases y objetos: primeros pasos

Intermedio ~20 min

Una clase agrupa estado (propiedades) y comportamiento (métodos). En Kotlin el molde es compacto pero muy expresivo.

class CuentaBancaria(val titular: String) {
    var saldo: Double = 0.0
        private set                 // solo la propia clase puede modificarlo

    fun deposita(cantidad: Double) {
        require(cantidad > 0) { "La cantidad debe ser positiva" }
        saldo += cantidad
    }

    fun retira(cantidad: Double): Boolean {
        if (cantidad > saldo) return false
        saldo -= cantidad
        return true
    }
}

fun main() {
    val c = CuentaBancaria("Ana")    // ¡sin new!
    c.deposita(500.0)

    println("${c.titular}: ${c.saldo}€")   // Ana: 500.0€
    println(c.retira(200.0))               // true
    // c.saldo = 999999.0                  // ERROR: setter privado
}

Propiedades, no getters/setters

Kotlin no tiene campos «desnudos»: toda variable de clase es una propiedad con getter implícito. Si necesitas lógica, defines el acceso tú mismo:

class Rectangulo(val ancho: Double, val alto: Double) {
    val area get() = ancho * alto           // propiedad CALCULADA
    val perimetro: Double
        get() = 2 * (ancho + alto)
}

val r = Rectangulo(3.0, 4.0)
println(r.area)      // 12.0  — se ve como campo, se calcula al vuelo

Visibilidad por defecto: public… y las demás

ModificadorVisible desde
public (default)Todas partes
privateSolo dentro del archivo/clase que lo declara
protectedClase y subclases (no existe a nivel top-level)
internalDentro del mismo módulo — ideal para APIs internas de librería

Puntos clave

  • Instancias sin new; propiedades con getters automáticos.
  • private set protege el estado manteniendo lectura pública.
  • internal es el modificador que Java no tiene.

18 · Constructores a fondo

Intermedio ~20 min

Kotlin distingue un constructor primario (en la cabecera) y constructores secundarios opcionales.

Constructor primario con validación: init

class Persona(val nombre: String, var edad: Int) {
    init {
        require(edad >= 0) { "La edad no puede ser negativa" }
        println("Nace $nombre")
    }
}

val p = Persona("Ana", 34)
// Persona("Luis", -5)   // IllegalArgumentException: La edad no puede ser negativa

Valores por defecto en lugar de sobrecargas

class Pizza(
    val tamano: String = "mediana",
    val quesoExtra: Boolean = false,
    val bordeRelleno: Boolean = false,
)

val miPizza = Pizza(quesoExtra = true)   // nombrados + defaults

Constructores secundarios: constructor

class Punto(val x: Int, val y: Int) {

    constructor(texto: String) : this(
        texto.substringBefore(',').trim().toInt(),
        texto.substringAfter(',').trim().toInt(),
    )

    override fun toString() = "Punto($x, $y)"
}

println(Punto("10, 20"))   // Punto(10, 20)

Objetos inmutables desde fábrica

Cuando la creación necesita lógica, el patrón companion object ofrece métodos estáticos de fábrica (capítulo 24 lo profundiza):

class Usuario private constructor(val email: String) {
    companion object {
        fun desdeEmail(email: String): Usuario? =
            if ("@" in email) Usuario(email) else null
    }
}

Usuario.desdeEmail("ana@x.com")   // Usuario@...
Usuario.desdeEmail("no-valido")   // null

Puntos clave

  • Constructor primario + bloque init para validar.
  • Defaults y argumentos nombrados eliminan casi todas las sobrecargas.
  • Fábricas en companion object para construcción con semántica.

19 · Propiedades avanzadas y delegación

Intermedio ~15 min

Custom getters y setters

class Temperatura {
    var celsius: Double = 20.0
        set(valor) {
            require(valor >= -273.15) { "Bajo el cero absoluto" }
            field = valor          // field = el campo de respaldo oculto
        }

    val fahrenheit get() = celsius * 9 / 5 + 32
}

val t = Temperatura()
t.celsius = 100.0
println(t.fahrenheit)   // 212.0
// t.celsius = -300.0   // IllegalArgumentException

Delegación de propiedades: by lazy, by observable…

Una propiedad puede «delegar» su lectura/escritura en otro objeto. La librería trae varios:

import kotlin.properties.Delegates

class Configuracion {
    val api: String by lazy { cargarApiCostosa() }   // solo al primer uso

    var tema: String by Delegates.observable("claro") { _, viejo, nuevo ->
        println("Tema: $viejo -> $nuevo")            // reacciona a cambios
    }
}

val c = Configuracion()
c.tema = "oscuro"     // Tema: claro -> oscuro

map como respaldo dinámico

class Json(val datos: Map<String, Any>) {
    val nombre: String   by datos
    val edad: Int        by datos
}

val j = Json(mapOf("nombre" to "Ana", "edad" to 34))
println("${j.nombre} tiene ${j.edad} años")   // Ana tiene 34 años

Este patrón es la base de cómo funcionan por debajo los ViewModels y preferencias en Android.

Delegación de INTERFAZ entre clases: by

interface Sonido { fun reproduce(): String }
class Mp3 : Sonido { override fun reproduce() = "♪ mp3" }

// ReproductorLog «envuelve» a otro Sonido sin reescribir sus métodos:
class ReproductorLog(private val base: Sonido) : Sonido by base {
    override fun reproduce() = "LOG: ${base.reproduce()}"
}

Puntos clave

  • field accede al respaldo dentro del setter; valida entradas ahí.
  • by lazy y Delegates.observable cubren 80 % de casos reales.
  • by también delega interfaces: decoradores sin boilerplate.

20 · Data classes: modelos sin boilerplate

Intermedio ~15 min

data class genera automáticamente equals, hashCode, toString, copy y acceso por posición. Es EL tipo para modelos, DTOs y estados.

data class Punto(val x: Int, val y: Int)

val p1 = Punto(1, 2)
val p2 = Punto(1, 2)

println(p1 == p2)       // true  ¡contenido!
println(p1)             // Punto(x=1, y=2)
println(p1.component1())// 1     acceso posicional

// copy: crea variaciones inmutables
val movido = p1.copy(x = 10)
println(movido)         // Punto(x=10, y=2)

Destructuring: desempaquetar objetos

val (x, y) = Punto(7, 3)
println(x + y)                    // 10

for ((nombre, precio) in mapOf("café" to 1.5)) println("$nombre: $precio")
val (min, max) = 3..9             // ¡los rangos también se destructuran!

Reglas y buenas prácticas

  • Solo propiedades del constructor primario participan en equals/hashCode/copy.
  • Prefiere todas val: data classes inmutables son seguras entre hilos.
  • No heredables entre sí útiles; combínalas con sealed classes (capítulo 23).
  • Máximo ~5 parámetros; más que eso, agrupa en otra data class.
// Estado de UI típico en Android moderno:
data class UiState(
    val cargando: Boolean = false,
    val usuarios: List<String> = emptyList(),
    val error: String? = null,
)

fun reduce(estado: UiState): UiState =
    estado.copy(cargando = false, usuarios = listOf("Ana"))

Puntos clave

  • equals/hashCode/toString/copy/componentN gratis.
  • copy() es la herramienta de la inmutabilidad funcional.
  • Destructuring hace el código declarativo (val (a, b) = ...).

21 · Herencia: open, super y override

Intermedio ~20 min

Kotlin invierte la filosofía de Java: todo está cerrado por defecto. Heredar exige que la clase y sus miembros estén marcados open. Menos herencias accidentales, más diseño consciente.

open class Animal(val nombre: String) {
    open fun sonido(): String = "..."

    fun duerme() = "$nombre duerme"    // NO open: idéntico para todos
}

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

class Gato(nombre: String, val vivo: Int = 9) : Animal(nombre) {
    override fun sonido() = "Miau"
    final override fun toString() = "Gato($nombre)"   // cierra de nuevo
}

val zoo = listOf(Perro("Rex"), Gato("Mishi"))
zoo.forEach { println("${it.nombre}: ${it.sonido()}") }
Rex: ¡Guau! Mishi: Miau

Polimorfismo y super

open class Notificacion(val mensaje: String) {
    open fun envia() = println("[base] $mensaje")
}

class Email(mensaje: String, val destino: String) : Notificacion(mensaje) {
    override fun envia() {
        super.envia()                       // reutiliza el comportamiento base
        println("       enviado a $destino")
    }
}

Email("Bienvenida", "ana@x.com").envia()

¿Herencia o composición?

Usa herencia cuando…Prefiere composición cuando…
Existe relación «es un» real y estableSolo necesitas «reusar código» («tiene un»)
El polimorfismo aporta valor (listas mezcladas)Quieres flexibilidad en tiempo de ejecución
Ej.: Vehiculo → CocheEj.: Coche + Motor, delegación con by

Puntos clave

  • Cerrado por defecto: open para permitir, override para extender.
  • final override vuelve a sellar un método.
  • Duda entre herencia/composición → composición con interfaces y by.

22 · Clases abstractas e interfaces

Intermedio ~20 min

Interface: contrato sin estado (o casi)

interface Volador {
    val altitudMax: Int                     // propiedad abstracta
    fun vuela() = "vuelo hasta ${altitudMax}m"   // implementación por defecto
}

interface Nadador {
    fun nada() = "nada como pez"
}

class Pato : Volador, Nadador {
    override val altitudMax = 1000
    // vuela() y nada() vienen gratis; solo implementa lo que difiera
}

Abstract class: estado compartido + plantilla

abstract class Reporte(val titulo: String) {
    private val pasos = mutableListOf<String>()

    protected abstract fun generaLineas(): List<String>   // obligatorio

    fun renderiza(): String {                             // plantilla común
        pasos += "inicio"
        val cuerpo = generaLineas()
        pasos += "fin"
        return "# $titulo\n" + cuerpo.joinToString("\n")
    }
}

class ReporteVentas(titulo: String, private val ventas: List<Double>) :
    Reporte(titulo) {

    override fun generaLineas() = listOf(
        "Total: ${ventas.sum()}",
        "Media: ${ventas.average()}",
    )
}

println(ReporteVentas("Agosto", listOf(100.0, 250.0)).renderiza())
# Agosto Total: 350.0 Media: 175.0

Cuál elegir

interfaceabstract class
Estado (campos)No (solo propiedades abstractas)
MúltipleSí (varias por clase)No (una sola base)
ConstructoresNoSí, con parámetros
Ideal para…Capacidades (puede volar)Esqueletos comunes (todo reporte hace X)

Puntos clave

  • Interfaces admiten defaults: migración suave de APIs.
  • Abstractas para el esqueleto con estado; interfaces para capacidades múltiples.
  • override es palabra obligatoria — nunca sobrescribes sin querer.

23 · Sealed classes: jerarquías cerradas

Avanzado ~20 min

Una jerarquía sealed declara todas sus subclases posibles en el mismo archivo. El compilador lo sabe y convierte a los when en exhaustivos: si añades un caso, todo el código que falta se marca en rojo.

sealed interface Resultado<out T> {
    data class Exito<T>(val datos: T) : Resultado<T>
    data class Error(val mensaje: String) : Resultado<Nothing>
    object Cargando : Resultado<Nothing>
}

fun maneja(r: Resultado<String>): String = when (r) {
    is Resultado.Exito    -> "OK: ${r.datos}"
    is Resultado.Error    -> "FALLO: ${r.mensaje}"
    Resultado.Cargando    -> "esperando…"
    // ¡sin rama else! El compilador verifica que cubres TODO.
}

El patrón Result: errores como valores

fun divide(a: Double, b: Double): Resultado<Double> =
    if (b != 0.0) Resultado.Exito(a / b)
    else Resultado.Error("División por cero")

println(maneja(divide(10.0, 4.0)))   // OK: 2.5
println(maneja(divide(1.0, 0.0)))    // FALLO: División por cero

Estados de UI sin estados imposibles

// MAL (flags sueltos): cargando=true y error="x" ¿a la vez?
// BIEN (sealed): cada estado es exactamente UNO:
sealed interface UiState {
    data object Vacia : UiState
    data class Cargada(val items: List<String>) : UiState
    data class Fallida(val causa: Throwable) : UiState
}

Esta tríada (data class + sealed + when exhaustivo) es el corazón de la arquitectura MVI/MVVM moderna en Android.

Smart casts automáticos con sealed

fun area(figura: Figura): Double = when (figura) {
    is Circulo   -> Math.PI * figura.radio * figura.radio   // figura ya es Circulo aquí
    is Rectangulo -> figura.base * figura.altura
}

sealed interface Figura
data class Circulo(val radio: Double) : Figura
data class Rectangulo(val base: Double, val altura: Double) : Figura

println(area(Circulo(1.0)))       // 3.14159...
println(area(Rectangulo(3.0, 2.0)))  // 6.0

Puntos clave

  • sealed = conjunto finito y conocido de subtipos.
  • when exhaustivo: añadir un caso rompe compilación donde toque.
  • Base de modelado de resultados, eventos y estados en apps reales.

24 · Objects: singleton, companion y anónimos

Intermedio ~15 min

Kotlin no tiene static. En su lugar, tres formas de «object»:

object declaration: singleton thread-safe gratis

object ConfigGlobal {
    var debug: Boolean = false
    val inicio: Long = System.currentTimeMillis()
}

ConfigGlobal.debug = true     // una única instancia, creada perezosamente

companion object: lo «estático» de una clase

class Sesion private constructor(val usuario: String) {
    companion object {
        const val MAX_INTENTOS = 3                 // constante real
        private var actual: Sesion? = null

        fun inicia(usuario: String): Sesion =
            actual ?: Sesion(usuario).also { actual = it }
    }
}

Sesion.MAX_INTENTOS                    // acceso tipo estático
val s = Sesion.inicia("ana")

object expression: implementaciones al vuelo

interface ClickListener { fun onClick(x: Int, y: Int) }

boton.registra(object : ClickListener {
    override fun onClick(x: Int, y: Int) = println("Click en ($x,$y)")
})

// Con interfaces de UN método, mejor una lambda:
boton.registra { x, y -> println("Click en ($x,$y)") }

data object: singletons con bonito toString

sealed interface Estado
data object SinConexion : Estado      // imprime "SinConexion", no la clase+hash

println(Estado.SinConexion)   // SinConexion

Puntos clave

  • object singleton perezoso y seguro entre hilos.
  • companion object agrupa constantes y fábricas de una clase.
  • Expresiones object para callbacks; lambdas si hay un solo método.

25 · Enums potenciados

Básico ~15 min

Los enum de Kotlin son clases completas: propiedades, métodos, interfaces y hasta comportamientos distintos por constante.

enum class Prioridad(val nivel: Int) {
    BAJA(1), MEDIA(2), ALTA(3), CRITICA(9);

    fun esUrgente() = nivel >= 3
}

val p = Prioridad.ALTA
println(p.nivel)          // 3
println(p.esUrgente())    // true
println(Prioridad.values().size)   // 4   (o Prioridad.entries, más moderno)
println(Prioridad.valueOf("MEDIA")) // MEDIA (lanza si no existe)

Comportamiento por constante

enum class Operacion {
    SUMAR { override fun aplica(a: Double, b: Double) = a + b },
    RESTAR { override fun aplica(a: Double, b: Double) = a - b };

    abstract fun aplica(a: Double, b: Double): Double
}

println(Operacion.SUMAR.aplica(2.0, 3.0))   // 5.0

Iteración y utilidades

for (op in Operacion.entries) print("${op.name} ")   // SUMAR RESTAR

val urgente = Prioridad.entries.firstOrNull { it.esUrgente() }
val porNivel = Prioridad.entries.sortedBy { it.nivel }
¿Enum o sealed class? Enum para constantes fijas con valores simples; sealed cuando cada variante necesita datos propios distintos (Exito(datos) vs Error(mensaje)).

Puntos clave

  • name, ordinal, entries incluidos.
  • Cada constante puede implementar métodos abstractos del propio enum.
  • Sealed class si las variantes llevan estructuras de datos distintas.

26 · Extensiones: el superpoder de Kotlin

Intermedio ~20 min

Las funciones de extensión añaden métodos a clases que no controlas (String, Int, librerías de terceros…) sin herencia ni wrappers.

// Sintaxis: fun TipoReceptor.nombreFuncion(...)
fun String.saluda(): String = "¡Hola, $this!"

println("Ana".saluda())          // ¡Hola, Ana!
// como si String hubiera nacido con ese método

Casos reales del día a día

// Formatear dinero:
fun Double.aEuros(): String = "%.2f €".format(this)
println(1234.5.aEuros())                    // 1234,50 €

// Validar email:
fun String?.esEmailValido(): Boolean =
    this != null && Regex(".+@.+\\..+").matches(this)

println("a@b.com".esEmailValido())          // true
println(null.esEmailValido())               // false  ¡extiende también el tipo anulable!

// Propiedades de extensión:
val String.inicial: Char get() = first()

// Genéricas:
fun <T> List<T>.segundaAlternativa(): T? = getOrNull(1)

Cómo resuelve Kotlin la ambigüedad

Las extensiones se resuelven estáticamente: gana la función miembro real de la clase si existe; entre extensiones, la más cercana en scope:

class A { fun hola() = "miembro" }

fun A.hola() = "extensión"
A().hola()     // "miembro" — la clase siempre gana

Ámbito: dónde viven tus extensiones

// Archivo Util.kt
package com.miapp.util
fun String.quitaAcentos(): String = ...   // pública, importable en todo el proyecto

class Procesador {
    private fun String.normaliza() = trim().lowercase()   // solo visible aquí dentro
}
Gran parte de la stdlib son extensiones: map, filter, let, toIntOrNull()… Ahora sabes cómo están hechas y puedes crear las tuyas.

Puntos clave

  • fun Tipo.nombre() extiende sin modificar la clase.
  • Pueden ser genéricas, sobre tipos anulables o privadas a un archivo/clase.
  • No son polimórficas: resolución estática; los miembros reales tienen prioridad.

27 · Genéricos: tipos parametrizados

Avanzado ~25 min

Los genéricos escriben algoritmos y estructuras una vez, para cualquier tipo, con seguridad en compilación.

// Función genérica:
fun <T> primero(lista: List<T>): T = lista.first()

println(primero(listOf(1, 2)))         // 1
println(primero(listOf("a", "b")))     // a

// Clase genérica: una caja para cualquier tipo
class Caja<T>(private val valor: T) {
    fun saca(): T = valor
}

val cInt = Caja(42)
val cStr = Caja("hola")
println(cInt.saca() + 1)    // 43  — el tipo viaja intacto

// Varios parámetros y restricciones:
fun <K, V> Map<K, V>.describe() = "$K claves"   // (ilustrativo)

fun <T : Comparable<T>> maximo(a: T, b: T): T =
    if (a > b) a else b        // T debe poder compararse consigo mismo

println(maximo(3, 7))          // 7
println(maximo("pera", "kiwi")) // pera

Varianza: in y out

interface Fuente<out T> { fun dame(): T }       // solo PRODUCE T → covariante (out)
class FuenteNums : Fuente<Number> { override fun dame(): Number = 42 }

val fuente: Fuente<Any> = FuenteNums()   // OK: Number "es un" Any al producirlo

interface Sumidero<in T> { fun recibe(x: T) }    // solo CONSUME T → contravariante (in)
  • out = posición de salida (productor). PECS: Producer out, Consumer in.
  • in = posición de entrada (consumidor).
  • Si no marcas nada (Caja<T>), es invariante: Caja<String> no es Caja<Any>.

Star projection y reified

fun cuentaStrings(lista: List<*>) = lista.count { it is String }

// reified conserva el tipo en tiempo de ejecución (solo inline):
inline fun <reified T : Any> Gson.fromJson(json: String): T = ...

val usuario: Usuario = gson.fromJson(json)   // ¡sin pasar Usuario::class.java!

Puntos clave

  • <T> parametriza funciones/clases; T : Bound limita.
  • out productor, in consumidor — sin wildcards crípticos.
  • reified + inline traen el tipo real a ejecución.

28 · Excepciones y manejo de errores

Intermedio ~20 min

Sintaxis idéntica a Java pero con un giro: no hay excepciones comprobadas (checked). Tú decides qué capturar, sin firmas llenas de throws.

fun leeEdad(texto: String): Int {
    return try {
        texto.toInt()
    } catch (e: NumberFormatException) {
        -1
    }
}

// try como EXPRESIÓN (devuelve valor):
val dividir = try { 10 / 2 } catch (e: ArithmeticException) { 0 }
println(dividir)   // 5

try/catch/finally completo

fun procesaArchivo(ruta: String) {
    var canal: java.nio.channels.FileChannel? = null
    try {
        // ... abrir y procesar ...
        throw IllegalStateException("archivo corrupto")
    } catch (e: java.io.IOException) {
        println("E/S falló: ${e.message}")
    } catch (e: Exception) {
        println("Error genérico: ${e.message}")
    } finally {
        println("cerrando recursos SIEMPRE")   // incluso si hubo excepción o return
    }
}

Lanzar y crear las tuyas

class SaldoInsuficienteException(val faltante: Double) :
    Exception("Faltan $faltante €")

fun retira(saldo: Double, cantidad: Double) {
    if (cantidad > saldo) throw SaldoInsuficienteException(cantidad - saldo)
}

try {
    retira(50.0, 80.0)
} catch (e: SaldoInsuficienteException) {
    println(e.message)     // Faltan 30.0 €
}

El estilo moderno Kotlin: errores como valores

// runCatching envuelve cualquier bloque en Result:
val resultado = runCatching { "abc".toInt() }

resultado.isSuccess            // false
resultado.getOrNull()          // null
resultado.exceptionOrNull()?.message

resultado.fold(
    onSuccess = { println("número: $it") },
    onFailure = { println("falló: ${it::class.simpleName}") },
)
// Combinado con sealed classes (capítulo 23) forma el patrón dominante en apps.
falló: NumberFormatException

Validación barata: require/check

fun registra(usuario: String?, edad: Int) {
    requireNotNull(usuario) { "usuario obligatorio" }  // IllegalArgumentException
    require(edad in 0..130) { "edad imposible: $edad" }
    check(!usuario.isBlank()) { "usuario vacío" }      // IllegalStateException
}
  • require: argumentos inválidos (fallo del que LLAMA).
  • check: estado interno roto (fallo del propio código).

Puntos clave

  • Sin checked exceptions; try es expresión.
  • runCatching/Result para errores esperables; excepciones para lo inesperado.
  • require/check/requireNotNull documentan contratos en una línea.

29 · Scope functions: let, run, apply, also, with

Avanzado ~25 min

Cinco funciones que ejecutan un bloque sobre un objeto y devuelven algo. Parecen iguales… hasta que aprendes la tabla mágica:

FunciónReferencia dentroDevuelveCuándo usarla
letitresultado del lambdaTransformar un valor no nulo; encadenar
runthisresultado del lambdaCalcular algo sobre el objeto
withthisresultado del lambdaAgrupar llamadas sobre un objeto ya existente
applythisel objetoConfigurar/construir (builder)
alsoitel objetoEfectos laterales: logs, validaciones…
data class Persona(var nombre: String = "", var edad: Int = 0)

// apply — configurar y seguir usándolo:
val ana = Persona().apply {
    nombre = "Ana"          // this.nombre implícito
    edad = 34
}
println(ana)                // Persona(nombre=Ana, edad=34)

// also — log sin interrumpir la cadena:
listOf(1, 2, 3).also { println("procesando $it") }.map { it * 2 }

// let — solo si no es null (capítulo 16):
val email: String? = "a@b.com"
email?.let { enviarA(it) }

// run — calcula y devuelve:
val resumen = ana.run { "$nombre ($edad años)" }
println(resumen)            // Ana (34 años)

// with — agrupación legible:
with(StringBuilder()) {
    append("línea 1\n"); append("línea 2")
    println(toString())
}

Truco mnemotécnico

¿Devuelvo el objeto?apply/also (para seguir encadenando)
¿Devuelvo otra cosa?let/run/with
¿Trabajo como miembro?this: apply/run/with
¿Objeto externo/nullable?it: let/also
No anides scope functions ni las uses «porque quedan modernas». Si un if o una variable local se entiende mejor, gana esa.

Puntos clave

  • apply para construir, let para nulos, also para efectos laterales.
  • run/with devuelven el resultado del bloque.
  • Mnemotecnia: ¿objeto o resultado? ¿this o it?

30 · Corutinas I: suspend, launch y async

Avanzado ~25 min

Las corutinas son concurrencia ligera y secuencial de leer: el código se escribe como síncrono pero no bloquea hilos. Necesitan la librería org.jetbrains.kotlinx:kotlinx-coroutines-core (capítulo 33).

import kotlinx.coroutines.*
import java.time.LocalTime

fun ahora() = LocalTime.now().toLocalTime().substring(0, 8)

fun main() = runBlocking {                 // puente para programas normales
    println("${ahora()} empiezo")

    launch {                               // «lanza y olvida»
        delay(1000)                        // NO bloquea el hilo: lo suspende
        println("${ahora()} terminó la tarea A")
    }

    val deferred = async {                 // devuelve un valor futuro
        delay(500)
        42
    }

    val resultado = deferred.await()       // espera el valor
    println("${ahora()} async devolvió $resultado")
}
12:00:00 empiezo 12:00:00 async devolvió 42 ← ¡no esperó a A! 12:00:01 terminó la tarea A

suspend: la marca que lo cambia todo

suspend fun descargaUsuario(id: Int): String {
    delay(300)                       // simula red
    return "Usuario$id"
}

suspend fun cargaPerfil() {
    // Secuencial: 600 ms
    val u1 = descargaUsuario(1)
    val u2 = descargaUsuario(2)

    // Paralelo: ~300 ms — dos corutinas a la vez
    coroutineScope {
        val d1 = async { descargaUsuario(3) }
        val d2 = async { descargaUsuario(4) }
        println("${d1.await()} y ${d2.await()}")
    }
}
  • Solo una función suspend puede llamar a otra suspend.
  • coroutineScope crea un ámbito estructurado: espera a sus hijos.
  • El compilador te obliga a ser consciente de qué código puede suspender.

Los tres lanzadores

HerramientaDevuelveUso
launchJob (sin valor)Efectos: logs, guardar, actualizar UI
asyncDeferred<T>Cálculos paralelos cuyo resultado necesitas
runBlockingTSolo en main() y tests — NUNCA dentro de apps

Puntos clave

  • delay() suspende sin bloquear; miles de corutinas caben en un hilo.
  • launch para efectos, async/await para valores.
  • runBlocking solo en la frontera (main/tests); dentro, scopes reales.

31 · Corutinas II: contextos y cancelación

Avanzado ~25 min

Dispatchers: ¿dónde corre mi código?

withContext(Dispatchers.IO) {        // red/disco: pool grande
    val datos = leeArchivoGrande()
}

val imagen = withContext(Dispatchers.Default) {   // CPU intensiva
    procesaPixeles(datos)
}

launch(Dispatchers.Main) {           // Android/UI: el hilo principal
    textView.text = "listo"
}

// Cambiar de contexto es barato y legible:
suspend fun cargaYProcesa() {
    val crudo = withContext(Dispatchers.IO) { bajaJson() }
    val modelo = withContext(Dispatchers.Default) { parsea(crudo) }
    withContext(Dispatchers.Main) { pinta(modelo) }
}

Estructura jerárquica: padres e hijos

fun main() = runBlocking {
    val padre = launch {
        launch { delay(10_000); println("hijo 1") }   // hereda el ciclo del padre
        launch { delay(10_000); println("hijo 2") }
        println("padre activo")
    }
    delay(100)
    padre.cancel()            // cancela TAMBIÉN a los hijos
    println("fin")            // los hijos nunca imprimen
}
  • Si un hijo falla, notifica al padre → cancelación en cascada (con Job normal).
  • SupervisorJob aísla fallos entre hermanos (típico en UIs).
  • El código cooperativo consulta su estado: isActive, o pasa por funciones cancellable como delay.

Timeouts y selectividad

withTimeout(2_000) {           // TimeoutCancellationException si excede
    descargaLenta()
}

// o mejor, un valor por defecto:
val datos = withTimeoutOrNull(2_000) { descargaLenta() } ?: CACHE_LOCAL

Compartir estado sin dolores

var contador = 0                       // MAL: condición de carrera

val contadorSeguro = AtomicInteger(0)  // atómico simple

// O confina todo a UN contexto:
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch { /* único dueño del estado mutable */ }

Puntos clave

  • Dispatchers.IO / Default / Main: elegir bien dónde corre cada cosa.
  • La estructura padre-hijo da cancelación en cascada gratis.
  • Timeouts con withTimeout(OrNull); estado compartido: atomics o confinamiento.

32 · Flow: streams reactivos

Avanzado ~25 min

Flow emite varios valores a lo largo del tiempo, de forma asíncrona y fría (no hace nada hasta que alguien lo recolecta). Es el «async iterable» oficial: ticks, sensores, bases de datos reactivas, estados de UI…

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun ticTac(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(200)            // trabajo suspendido entre emisiones
        emit(i)               // publica un valor
    }
}

fun main() = runBlocking {
    ticTac().collect { valor ->
        println("recibido $valor")
    }
}
recibido 1 recibido 2 recibido 3

Operadores: transformar streams como listas

tictac
ticTac()
    .filter { it % 2 == 1 }         // solo impares
    .map { it * 10 }                // transforma
    .onEach { println("emitido $it") }
    .take(2)                        // primeros dos y cancela la fuente
    .collect { ... }
emitido 10 emitido 30

Flujos de estado y compartidos

// StateFlow: SIEMPRE tiene un valor actual — ideal para pantalla:
private val _estado = MutableStateFlow(UiState())
val estado: StateFlow<UiState> = _estado.asStateFlow()   // lectura externa

fun carga() = scope.launch {
    _estado.value = UiState(cargando = true)
    val datos = repositorio.descarga()
    _estado.value = UiState(usuarios = datos)
}

// sharedFlow: eventos sin valor inicial (toasts, navegación):
private val _eventos = MutableSharedFlow<Evento>()
val eventos = _eventos.asSharedFlow()

Cold vs hot, en una tabla

flow {}StateFlow/SharedFlow
ArranqueAl collect (frío)Vive aunque nadie escuche (caliente)
Múltiples coleccionistasCada uno reinicia el flujoComparten la misma fuente
Uso típicoPeticiones puntualesEstado observable de ViewModel
Errores dentro del flow se manejan con catch { emit(Error(...)) } o .catch{}, y timeouts/cancelación funcionan igual que las corutinas normales (capítulo 31).

Puntos clave

  • flow { emit() } + collect: frío por defecto.
  • Mismos operadores que colecciones: filter/map/take… pero en el tiempo.
  • StateFlow para estado, SharedFlow para eventos.

33 · Proyectos, Gradle y dependencias

Intermedio ~20 min

Gradle es el sistema de construcción estándar de Kotlin y Android. Su configuración se escribe… ¡en Kotlin! (.kts).

// build.gradle.kts de una app JVM:
plugins {
    kotlin("jvm") version "2.1.0"
    application                       // añade tareas run/installDist
}

group = "com.miapp"
version = "1.0.0"

repositories { mavenCentral() }       // dónde buscar dependencias

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
    testImplementation(kotlin("test"))   // kotlin.test para pruebas
}

application { mainClass.set("MainKt") }   // clase generada con tu fun main

Tareas del día a día

gradle tasks # lista todo lo disponible gradle build # compila + tests + empaqueta gradle test # solo pruebas (capítulo 35) gradle run # ejecuta mainClass gradle clean build # desde cero

En Windows usa gradlew.bat; el wrapper (./gradlew) garantiza la misma versión de Gradle en cualquier máquina.

Añadir una librería: tres pasos

  1. Búscala en Maven Central o mvnrepository.com.
  2. Copia la línea implementation(...) que ofrece la web.
  3. gradle build descarga y enlaza; importa y listo:
import kotlinx.serialization.json.Json

val json = Json.encodeToString(mapOf("ok" to true))   // con kotlinx-serialization-json

Version catalogs (proyectos modernos)

Kotlin/Android serios centralizan versiones en gradle/libs.versions.toml:

[versions] coroutines = "1.9.0" [libraries] coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

Puntos clave

  • build.gradle.kts: plugins, repositorios, dependencies.
  • implementation interna vs api expuesta a consumidores.
  • Wrapper ./gradlew = reproducibilidad garantizada.

34 · Utilidades esenciales: fechas, Regex, archivos y JSON

Intermedio ~25 min

Fechas: java.time (y kotlinx-datetime)

import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val hoy = LocalDate.now()
val cita = LocalDate.of(2026, 12, 24)

println(cita.dayOfWeek)                 // THURSDAY
println(cita.plusDays(7))               // 2026-12-31
println(hoy.isBefore(cita))             // true

val fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")
println(LocalDateTime.now().format(fmt))     // 22/08/2026 17:45
println(LocalDate.parse("01/03/2027", DateTimeFormatter.ofPattern("dd/MM/yyyy")))

En multiplataforma pura, la librería oficial kotlinx-datetime ofrece las mismas ideas con API propia.

Regex con sintaxis Kotlin

val patron = Regex("""\d{2}/\d{2}/\d{4}""")   // raw string sin dobles contrabarras

println(patron.matches("22/08/2026"))         // true (coincidencia completa)
println(patron.containsMatchIn("vence el 01/01/2027"))   // true
println(patron.findAll(texto).map { it.value }.toList())

// Reemplazo con grupos:
val tel = Regex("""(\d{3})-(\d{4})""")
println(tel.replace("llama al 555-1234") { "(${it.groupValues[1]}) ${it.groupValues[2]}" })
// llama al (555) 1234

Archivos: kotlin.io + NIO

import java.io.File

File("notas.txt").writeText("primera línea\n")
File("notas.txt").appendText("segunda\n")

println(File("notas.txt").readText())
File("notas.txt").readLines().forEachIndexed { i, l -> println("$i: $l") }

// Recorrido recursivo:
File(".").walkTopDown()
    .filter { it.extension == "kt" }
    .take(5)
    .forEach(::println)

JSON: kotlinx.serialization

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

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class Articulo(val titulo: String, val precio: Double)

val texto = """{"titulo":"Libro","precio":19.9}"""
val art = Json.decodeFromString<Articulo>(texto)
println(art.precio)                          // 19.9
println(Json.encodeToString(art.copy(precio = 25.0)))
{"titulo":"Libro","precio":25.0}

Puntos clave

  • java.time inmutable y moderna; formateadores explícitos.
  • Regex como objeto Regex(raw string) reutilizable.
  • kotlinx.serialization convierte data classes ↔ JSON por reflejo de compilación.

35 · Pruebas unitarias con kotlin.test

Intermedio ~20 min

Las pruebas viven en src/test/kotlin, espejando los paquetes del código real. kotlin("test") funciona sobre JUnit y es multiplataforma.

// src/main/kotlin/Calculadora.kt
class Calculadora {
    fun divide(a: Double, b: Double): Double {
        require(b != 0.0) { "división por cero" }
        return a / b
    }
}
// src/test/kotlin/CalculadoraTest.kt
import kotlin.test.*

class CalculadoraTest {
    private val calc = Calculadora()

    @Test
    fun `divide numeros normales`() = assertEquals(2.5, calc.divide(10.0, 4.0))

    @Test
    fun `divide por cero lanza`() {
        val ex = assertFailsWith<IllegalArgumentException> { calc.divide(1.0, 0.0) }
        assertTrue(ex.message!!.contains("cero"))
    }

    @Test
    fun `precision de coma flotante con delta`() =
        assertEquals(0.3, 0.1 + 0.2, absoluteTolerance = 1e-9)
}
./gradlew test # ejecuta todas las pruebas # informe HTML en build/reports/tests/test/index.html

Aserciones principales

AserciónVerifica
assertEquals(esperado, actual)Igualdad (con tolerancia para doubles)
assertNotEquals / assertSameDesigualdad / misma referencia
assertTrue / assertFalseCondiciones booleanas
assertNull / assertNotNullNulabilidad (¡la segunda hace smart cast!)
assertFailsWith<E>{ }Que un bloque lance exactamente E
assertFailsWith` + mensajeCombinar tipo + texto del error

Ciclo de vida y tests de corutinas

class RepositorioTest {
    @BeforeTest fun prepara() { /* datos frescos por test */ }
    @AfterTest  fun limpia()  { /* cierra recursos */ }
}

// Para suspend fun: runTest (kotlinx-coroutines-test)
@Test fun `carga usuario`() = runTest {
    val u = repositorio.usuario(1)
    assertEquals("Ana", u.nombre)
}

Puntos clave

  • Estructura espejo main/test; nombres entre tildes inversas legibles.
  • assertFailsWith para excepciones esperadas.
  • runTest para probar funciones suspend sin esperas reales.

36 · Interoperabilidad con Java

Avanzado ~20 min

Kotlin fue diseñado para convivir con Java al 100 %: puedes llamar librerías Java desde Kotlin y viceversa, mezclando archivos en el mismo proyecto Gradle.

Usar APIs Java desde Kotlin

import java.util.HashMap

val mapa = HashMap<String, Int>()   // tipos genéricos Java, sintaxis Kotlin
mapa.put("a", 1)

// Los getters/setters Java se ven como propiedades:
val calendario = java.util.Calendar.getInstance()
println(calendario.time)             // getTime() -> propiedad .time

val lista = java.util.ArrayList<String>()
lista += "desde Kotlin"              // operadores mapeados a add()

Tipos plataforma y nulabilidad heredada

Java no distingue anulable/no anulable → llegan como String! («tipo plataforma»). Tú decides cómo tratarlos:

val nombreJava: String? = apiJava.getNombre()   // defensivo (recomendado)
val seguro = nombreJava ?: "invitado"

Anotaciones para pulir tu API hacia Java

@file:JvmName("StringUtils")     // nombre de clase Java amigable

class Cofre(private val secreto: String) {
    @JvmField val id = 7                    // campo público directo

    companion object {
        @JvmStatic fun fabrica() = Cofre("x")   // static real para Java
    }
}
// En Java: import static StringUtilsKt.*; // o StringUtils según @JvmName Cofre c = Cofre.fabrica(); System.out.println(c.id); // acceso directo gracias a @JvmField

Sobrecarga de operadores Java «escondidos»

// Los métodos Java con firma especial se usan como operadores:
val sb = StringBuilder("abc")
sb += "def"                 // append()

for ((i, c) in "texto".withIndex()) {}   // Iterable de Java: for-each natural

// SAM conversion: interfaz Java de un método ← lambda directa
boton.addActionListener { e -> println("pulsado") }

Puntos clave

  • Mix Kotlin/Java en el mismo módulo Gradle sin configuración extra.
  • Cuidado con tipos plataforma: trata lo que viene de Java como anulable.
  • @JvmStatic/@JvmField/@JvmName hacen tus APIs cómodas desde Java.

37 · Buenas prácticas y código idiomático

Intermedio ~20 min

Antes / después: escribir «en Kotlin»

// ❌ Java disfrazado:
val lista: MutableList<Int> = ArrayList()
for (i in 0 until 10) {
    if (i % 2 == 0) {
        lista.add(i * i)
    }
}

// ✅ Kotlin idiomático:
val cuadrados = (0..9).filter { it % 2 == 0 }.map { it * it }
// ❌ Nulos a la Java:
if (usuario != null && usuario.email != null) {
    enviar(usuario.email)
}

// ✅ Encadenado seguro:
usuario?.email?.let(::enviar)

// ❌ Builder manual con setters:
val cfg = Config()
cfg.host = "x"; cfg.puerto = 8080

// ✅ apply + defaults + data class:
val cfg = Config(host = "x").apply { puerto = 8080 }

Checklist de estilo

  • val por defecto; inmutabilidad primero.
  • Expresiones sobre sentencias: if/when/try devuelven valor — úsalo.
  • Nombres que digan: funciones verbo (calculaTotal), clases sustantivo, booleanos es/tiene.
  • Una responsabilidad por función; extrae funciones locales si crece.
  • Sealed + when en vez de flags booleanos múltiples.
  • Sin !!, sin capturas vacías, sin comentarios que expliquen código ilegible: reescríbelo.

Herramientas que imponen el estilo

# ktlint — linter de estilo oficial de la comunidad curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod +x ktlint ./ktlint "src/**/*.kt" --format # detekt — análisis estático profundo (code smells, complejidad) # integrable como plugin de Gradle

La referencia oficial

Lee Coding Conventions e Idioms en kotlinlang.org. Son cortos y marcan el estándar que seguirá cualquier equipo.

Puntos clave

  • Inmutabilidad + expresiones + pipeline funcional = Kotlin idiomático.
  • ktlint/detekt automatizan lo subjetivo.
  • Las convenciones oficiales son lectura obligatoria antes de trabajar en equipo.

38 · Rumbo a Android y Kotlin Multiplatform

Meta final ~15 min

¡Lo lograste! Dominas el lenguaje: desde val hasta corutinas y Flow. Ahora el mapa de lo que sigue.

Camino A · Android nativo con Jetpack Compose

  1. Instala Android Studio (incluye SDK, emulador y Gradle ya configurados).
  2. Crea un proyecto «Empty Activity» con plantilla Compose: verás funciones @Composable — ¡solo Kotlin del curso!
  3. @Composable
    fun Saludo(nombre: String) {
        Text(text = "Hola $nombre")     // UI declarada con funciones Kotlin
    }
  4. Estudia: estados (State/remember), navegación, Room (BD), ViewModel + StateFlow — todo construido sobre lo aprendido en los capítulos 30–32 y 23.
  5. Sigue el curso oficial: Android Basics with Compose.

Camino B · Servidor y backend

  • Ktor: framework web asíncrono hecho con corutinas y extensiones.
    embeddedServer(Netty, port = 8080) {
        routing {
            get("/hola/{nombre}") {
                call.respondText("¡Hola, ${call.parameters["nombre"]}!")
            }
        }
    }.start(wait = true)
  • Otros: Spring Boot (soporte Kotlin de primera clase), Exposed para BD.

Camino C · Kotlin Multiplatform (KMP)

Comparte lógica de negocio entre Android, iOS, escritorio y web manteniendo la UI nativa (o compartida también, con Compose Multiplatform):

mi-app/ ├── composeApp/ # código COMPARTIDO (commonMain) + específico por plataforma │ └── src/ │ ├── commonMain/kotlin/ # tu lógica: modelos, repositorios, ViewModels │ ├── androidMain/kotlin/ │ └── iosMain/kotlin/ ├── iosApp/ # proyecto Xcode que consume el framework generado └── gradle/libs.versions.toml

Tu plan de 30 días sugerido

SemanaObjetivo
1–2Android Basics módulos 1–3; una app de lista con Room
3ViewModel + StateFlow + navegación; publica tu primera APK
4Tests con runTest, ktlint en CI, y clona un proyecto KMP de ejemplo

Puntos clave

  • Tres caminos sólidos: Android/Compose, backend/Ktor, multiplataforma/KMP.
  • Todo lo aprendido aquí es literalmente el lenguaje de esos ecosistemas.
  • Practica con proyectos pequeños reales antes de escalar.