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.
1 · ¿Qué es Kotlin?
Básico ~10 minKotlin 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
Kotlin frente a otros lenguajes
| Lenguaje | Relación con Kotlin |
|---|---|
Java | Kotlin compila al mismo bytecode y convive con él; casi siempre «Java menos ruido» con extras. |
Scala | Similar potencia funcional, con curva mucho más amable y compilación rápida. |
Swift | Filosofía gemela (null safety, protocolos/extensión, optionals); Kotlin corre donde Swift no llega. |
Dart | Ambos 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") }
}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 minNecesitas 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.
2.1 · Paso previo universal: instalar un JDK
Cualquier JDK 17 o superior vale. Recomendado: Eclipse Temurin (gratuito, mantenido).
2.2 · Compilador Kotlin en Linux (SDKMAN!, la vía cómoda)
SDKMAN! también gestiona versiones de Java
(sdk install java 21-tem) y actualiza con
sdk upgrade kotlin.
2.3 · Compilador Kotlin en Windows
- Descarga
kotlin-compiler-x.y.z.zipdesde GitHub Releases. - Descomprime en
C:\kotlinc. - Añade
C:\kotlinc\kotlinc\bina la variablePathde tu usuario (menú inicio → «variables de entorno»). - Abre una terminal nueva y ejecuta
kotlinc -version.
2.4 · Editor: IntelliJ IDEA Community
- Descarga IntelliJ IDEA Community Edition (gratis) desde jetbrains.com/idea.
- Al crear tu primer proyecto Kotlin, el asistente puede descargar el JDK solo («Download JDK», Temurin 21).
- Trae autocompletado inteligente, refactorizaciones, depurador, inspecciones y formateo. Es el estándar del mundo Kotlin.
- 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 min3.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
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:
- New Project → Kotlin → JVM → marca «Gradle» (o «IntelliJ» para empezar simple).
- Crea
Main.kt, escribemainy pulsa Tab: el IDE genera la función por ti. - Pulsa el triángulo verde junto a
fun main()→ Run.
3.3 · Estructura de un proyecto con Gradle
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}")
}valdeclara un valor inmutable (lo normal en Kotlin).$saludoinserta variables dentro del texto;${expresión}evalúa código.- Para recibir argumentos reales:
fun main(args: Array<String>).
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
.ktso el botón Run de IDEA. - Proyecto estándar: Gradle con
src/main/kotlinysrc/test/kotlin.
4 · Sintaxis básica: reglas del juego
Básico ~12 minKotlin 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 JavaPuntos 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 minKotlin no tiene «variables mutables por defecto»: tienes que decidirlo tú. Es una decisión consciente que el compilador aprovecha.
| Palabra clave | Mutabilidad | Cuándo usarla |
|---|---|---|
val | Solo lectura (referencia inmutable) | Por defecto. El 90–95 % de tu código |
var | Lectura/escritura (mutable) | Solo cuando el valor cambia con el tiempo (contadores, acumuladores) |
const val | Constante de compilación | Solo 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 noDeclaració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: solovar, solo tipos no primitivos; lanza excepción si aún no está lista.by lazy: sobreval; thread-safe por defecto e ideal para valores costosos.
val.
Cambia a var únicamente cuando el compilador te obligue. Tu código futuro
(y tus compañeros) lo agradecerán.Puntos clave
valpor defecto,varjustificado,const valpara constantes globales.- La inferencia de tipos evita anotarlos en el 95 % de los casos.
lateinityby lazycubren la inicialización diferida.
6 · Números, caracteres y booleanos
Básico ~15 minA 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:
| Tipo | Tamaño | Rango / uso típico |
|---|---|---|
Byte | 8 bits | -128 a 127 |
Short | 16 bits | -32.768 a 32.767 |
Int | 32 bits | El entero por defecto (±2,1 mil millones) |
Long | 64 bits | Enteros grandes; literal con L: 3_000_000_000L |
Float | 32 bits | Decimal de precisión simple; literal con F |
Double | 64 bits | El 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 positivoChar: 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
IntyDoublecubren el 95 % de casos;Longpara IDs grandes.- Conversiones SIEMPRE explícitas (
toLong(),toDouble()…). - División entera trunca; usa
floorDiv/modpara semántica matemática.
7 · Cadenas de texto (String) a fondo
Básico ~20 minLos 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") // trueComparació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 contrabarrastrimIndent() 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 printftoInt() 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 minOperadores 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) // 123Operadores lógicos y de identidad
| Operador | Significado |
|---|---|
&&, ||, ! | Y, O, NO lógicos (con cortocircuito) |
== / != | Igualdad de contenido (llama a equals) |
=== / !== | Igualdad de referencia (mismo objeto en memoria) |
in / !in | Pertenece a colección o rango |
is / !is | Comprobació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 letrasx 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,downToysteplos afinan.insirve para pertenencia e iteración.isactiva smart casts sin conversiones manuales.
9 · Condicionales: if y when
Básico ~15 minif 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"
}- 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
elsees 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) // NotablePuntos clave
- No existe el operador ternario: usa
ifcomo expresión. whensustituye 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 minfor: 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 casosPuntos clave
foritera rangos, colecciones, strings… sin contadores manuales.withIndex(),break/continuecon etiquetas cubren los casos complejos.- Prefiere operaciones funcionales cuando puedas; guarda
whilepara condiciones dinámicas.
11 · Funciones I: los fundamentos
Básico ~15 minSe 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)) // 20Argumentos 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:3000Funciones 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.5Puntos 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 minvararg: 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 arrayOrden recomendado de parámetros
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.0Funciones 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)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.
infixyoperatorpermiten APIs casi naturales.
13 · Lambdas y funciones como valores
Intermedio ~20 minUna 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 claseClosures: 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()) // 3inline: 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 };itcuando hay un solo parámetro. - Trailing lambda: el último argumento sale del paréntesis.
::funcionconvierte funciones existentes en valores.inlineelimina el overhead de objetos lambda.
14 · Colecciones I: List e Iterable
Intermedio ~20 minKotlin 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 lectura | Mutable | Qué 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, TecladoReducciones 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/foldresuelven consultas sin bucles manuales.
15 · Colecciones II: Set y Map
Intermedio ~20 minSet: 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 actualIteració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]}groupBy, associateBy y
associateWith los generan desde listas.Puntos clave
Set: unicidad + pertenencia O(1); operacionesunion/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 minLa 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 usuarioSmart 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!! 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 deliberadosPuntos 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 minUna 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 vueloVisibilidad por defecto: public… y las demás
| Modificador | Visible desde |
|---|---|
public (default) | Todas partes |
private | Solo dentro del archivo/clase que lo declara |
protected | Clase y subclases (no existe a nivel top-level) |
internal | Dentro del mismo módulo — ideal para APIs internas de librería |
Puntos clave
- Instancias sin
new; propiedades con getters automáticos. private setprotege el estado manteniendo lectura pública.internales el modificador que Java no tiene.
18 · Constructores a fondo
Intermedio ~20 minKotlin 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 negativaValores 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 + defaultsConstructores 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") // nullPuntos clave
- Constructor primario + bloque
initpara validar. - Defaults y argumentos nombrados eliminan casi todas las sobrecargas.
- Fábricas en
companion objectpara construcción con semántica.
19 · Propiedades avanzadas y delegación
Intermedio ~15 minCustom 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 // IllegalArgumentExceptionDelegació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 -> oscuromap 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ñosEste 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
fieldaccede al respaldo dentro del setter; valida entradas ahí.by lazyyDelegates.observablecubren 80 % de casos reales.bytambién delega interfaces: decoradores sin boilerplate.
20 · Data classes: modelos sin boilerplate
Intermedio ~15 mindata 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 minKotlin 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()}") }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 estable | Solo necesitas «reusar código» («tiene un») |
| El polimorfismo aporta valor (listas mezcladas) | Quieres flexibilidad en tiempo de ejecución |
Ej.: Vehiculo → Coche | Ej.: Coche + Motor, delegación con by |
Puntos clave
- Cerrado por defecto:
openpara permitir,overridepara extender. final overridevuelve a sellar un método.- Duda entre herencia/composición → composición con interfaces y
by.
22 · Clases abstractas e interfaces
Intermedio ~20 minInterface: 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())Cuál elegir
| interface | abstract class | |
|---|---|---|
| Estado (campos) | No (solo propiedades abstractas) | Sí |
| Múltiple | Sí (varias por clase) | No (una sola base) |
| Constructores | No | Sí, 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.
overridees palabra obligatoria — nunca sobrescribes sin querer.
23 · Sealed classes: jerarquías cerradas
Avanzado ~20 minUna 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 ceroEstados 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.0Puntos clave
sealed= conjunto finito y conocido de subtipos.whenexhaustivo: 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 minKotlin 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 perezosamentecompanion 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) // SinConexionPuntos clave
objectsingleton perezoso y seguro entre hilos.companion objectagrupa constantes y fábricas de una clase.- Expresiones object para callbacks; lambdas si hay un solo método.
25 · Enums potenciados
Básico ~15 minLos 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.0Iteració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 }
Exito(datos) vs Error(mensaje)).Puntos clave
name,ordinal,entriesincluidos.- 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 minLas 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étodoCasos 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
}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 minLos 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")) // peraVarianza: 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 esCaja<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 : Boundlimita.outproductor,inconsumidor — sin wildcards crípticos.reified+ inline traen el tipo real a ejecución.
28 · Excepciones y manejo de errores
Intermedio ~20 minSintaxis 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) // 5try/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.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;
tryes expresión. runCatching/Resultpara errores esperables; excepciones para lo inesperado.require/check/requireNotNulldocumentan contratos en una línea.
29 · Scope functions: let, run, apply, also, with
Avanzado ~25 minCinco funciones que ejecutan un bloque sobre un objeto y devuelven algo. Parecen iguales… hasta que aprendes la tabla mágica:
| Función | Referencia dentro | Devuelve | Cuándo usarla |
|---|---|---|---|
let | it | resultado del lambda | Transformar un valor no nulo; encadenar |
run | this | resultado del lambda | Calcular algo sobre el objeto |
with | this | resultado del lambda | Agrupar llamadas sobre un objeto ya existente |
apply | this | el objeto | Configurar/construir (builder) |
also | it | el objeto | Efectos 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
apply/also (para seguir encadenando)let/run/withthis: apply/run/withit: let/alsoif o una variable local se entiende mejor, gana esa.Puntos clave
applypara construir,letpara nulos,alsopara efectos laterales.run/withdevuelven el resultado del bloque.- Mnemotecnia: ¿objeto o resultado? ¿this o it?
30 · Corutinas I: suspend, launch y async
Avanzado ~25 minLas 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")
}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
suspendpuede llamar a otrasuspend. coroutineScopecrea un ámbito estructurado: espera a sus hijos.- El compilador te obliga a ser consciente de qué código puede suspender.
Los tres lanzadores
| Herramienta | Devuelve | Uso |
|---|---|---|
launch | Job (sin valor) | Efectos: logs, guardar, actualizar UI |
async | Deferred<T> | Cálculos paralelos cuyo resultado necesitas |
runBlocking | T | Solo en main() y tests — NUNCA dentro de apps |
Puntos clave
delay()suspende sin bloquear; miles de corutinas caben en un hilo.launchpara efectos,async/awaitpara valores.runBlockingsolo en la frontera (main/tests); dentro, scopes reales.
31 · Corutinas II: contextos y cancelación
Avanzado ~25 minDispatchers: ¿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
Jobnormal). SupervisorJobaísla fallos entre hermanos (típico en UIs).- El código cooperativo consulta su estado:
isActive, o pasa por funciones cancellable comodelay.
Timeouts y selectividad
withTimeout(2_000) { // TimeoutCancellationException si excede
descargaLenta()
}
// o mejor, un valor por defecto:
val datos = withTimeoutOrNull(2_000) { descargaLenta() } ?: CACHE_LOCALCompartir 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 minFlow 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")
}
}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 { ... }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 | |
|---|---|---|
| Arranque | Al collect (frío) | Vive aunque nadie escuche (caliente) |
| Múltiples coleccionistas | Cada uno reinicia el flujo | Comparten la misma fuente |
| Uso típico | Peticiones puntuales | Estado observable de ViewModel |
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.
StateFlowpara estado,SharedFlowpara eventos.
33 · Proyectos, Gradle y dependencias
Intermedio ~20 minGradle 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 mainTareas del día a día
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
- Búscala en Maven Central o mvnrepository.com.
- Copia la línea
implementation(...)que ofrece la web. gradle builddescarga y enlaza; importa y listo:
import kotlinx.serialization.json.Json
val json = Json.encodeToString(mapOf("ok" to true)) // con kotlinx-serialization-jsonVersion catalogs (proyectos modernos)
Kotlin/Android serios centralizan versiones en gradle/libs.versions.toml:
Puntos clave
build.gradle.kts: plugins, repositorios, dependencies.implementationinterna vsapiexpuesta a consumidores.- Wrapper
./gradlew= reproducibilidad garantizada.
34 · Utilidades esenciales: fechas, Regex, archivos y JSON
Intermedio ~25 minFechas: 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) 1234Archivos: 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)))Puntos clave
java.timeinmutable 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 minLas 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)
}Aserciones principales
| Aserción | Verifica |
|---|---|
assertEquals(esperado, actual) | Igualdad (con tolerancia para doubles) |
assertNotEquals / assertSame | Desigualdad / misma referencia |
assertTrue / assertFalse | Condiciones booleanas |
assertNull / assertNotNull | Nulabilidad (¡la segunda hace smart cast!) |
assertFailsWith<E>{ } | Que un bloque lance exactamente E |
assertFailsWith` + mensaje | Combinar 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.
assertFailsWithpara excepciones esperadas.runTestpara probar funciones suspend sin esperas reales.
36 · Interoperabilidad con Java
Avanzado ~20 minKotlin 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
}
}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/@JvmNamehacen tus APIs cómodas desde Java.
37 · Buenas prácticas y código idiomático
Intermedio ~20 minAntes / 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, booleanoses/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
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
- Instala Android Studio (incluye SDK, emulador y Gradle ya configurados).
- Crea un proyecto «Empty Activity» con plantilla Compose: verás funciones
@Composable— ¡solo Kotlin del curso! @Composable fun Saludo(nombre: String) { Text(text = "Hola $nombre") // UI declarada con funciones Kotlin }- Estudia: estados (
State/remember), navegación, Room (BD), ViewModel + StateFlow — todo construido sobre lo aprendido en los capítulos 30–32 y 23. - 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):
Tu plan de 30 días sugerido
| Semana | Objetivo |
|---|---|
| 1–2 | Android Basics módulos 1–3; una app de lista con Room |
| 3 | ViewModel + StateFlow + navegación; publica tu primera APK |
| 4 | Tests 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.