Skip to content

Kotlin Hoja de referencia

Lenguaje JVM moderno, conciso y totalmente interoperable con Java.

01

Fundamentos

Variables y Tipos

Prefiere val (inmutable) sobre var (mutable). Kotlin infiere tipos pero permite anotaciones explícitas. Usa 'is' para type checks (smart-casts automáticamente). Todo es un object—no hay primitivos en la sintaxis de Kotlin.

kotlin
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List<Int> = listOf(1, 2, 3)
println(name::class)  // class kotlin.String
println(age is Int)   // true

String Templates

$var para variables simples, ${expr} para expresiones. Las triple-quoted strings preservan newlines—usa trimIndent() para formateo limpio. Los string templates hacen la construcción de strings concisa y legible sin format strings.

kotlin
val name = "Alice"
val age = 30
println("Name: $name, Age: $age")  // simple var
println("Length: ${name.length}")  // expression
println("Upper: ${name.uppercase()}")
val multiline = """
  Hello, $name
  Age: $age
""".trimIndent()

Null Safety

Null safety de Kotlin: ? marca tipos nullable, ?. safe call (retorna null si es null), !! afirma non-null (lanza NPE si es null), ?: elvis (default si es null). Esto elimina NullPointerException en tiempo de compilación—característica core de Kotlin.

kotlin
var name: String? = null  // nullable type
println(name?.length)     // null (safe call)
name = "Alice"
println(name!!.length)    // 5 (non-null assertion)
val len: Int = name?.length ?: 0  // elvis operator
// Non-null types can never be null
val s: String = "hi"  // not nullable

Conversión de Tipos

Kotlin requiere conversión explícita de tipos (sin widening implícito como Java). Usa toInt()/toDouble()/toString() para conversión. toIntOrNull() retorna null en fallo—más seguro que toInt() que lanza. Siempre maneja null del parsing.

kotlin
val n: Int = 42
val d: Double = n.toDouble()
val s: String = n.toString()
val fromStr: Int? = "100".toIntOrNull()
val invalid: Int? = "abc".toIntOrNull()  // null
println(d, s, fromStr, invalid)

Type Checks & Smart Cast

is verifica el tipo y smart-casts la variable en esa rama—sin cast explícito necesario. !is es la negación. Usa as para casts inseguros (lanza ClassCastException), as? para casts seguros (retorna null en fallo). Smart cast es una gran conveniencia de Kotlin.

kotlin
fun describe(x: Any): String =
    when (x) {
        is Int -> "Int: ${x + 1}"  // x smart-cast to Int
        is String -> "String of length ${x.length}"
        is List<*> -> "List with ${x.size} items"
        else -> "Unknown"
    }
println(describe(42))      // Int: 43
println(describe("hello")) // String of length 5
02

Strings

Métodos Comunes de String

Los strings de Kotlin tienen ricas extension functions de la biblioteca estándar. La mayoría retornan nuevos strings (inmutables). Usa estos en lugar de loops manuales. Funciones como uppercase() son locale-aware—usa uppercase(Locale.ROOT) para resultados consistentes.

kotlin
val s = "Hello, World"
println(s.length)          // 12
println(s.uppercase())     // HELLO, WORLD
println(s.lowercase())     // hello, world
println(s.split(", "))     // [Hello, World]
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reversed())      // dlroW ,olleH
println(s.startsWith("Hello"))  // true

Multiline & Raw Strings

Las triple-quoted strings son raw—no necesitan secuencias de escape (excepto triple double-quotes). trimMargin('|') mantiene solo el texto después de | para indentación limpia. trimIndent() elimina el whitespace común. Ideal para SQL, JSON, HTML en código.

kotlin
val text = """
    |Hello,
    |World!
""".trimMargin()  // | marks line start
println(text)
val json = """{"name": "Alice", "age": 30}"""
println(json)  // no escape needed for quotes
// trimIndent() removes common leading whitespace

Construcción de Strings

joinToString es la forma idiomática de unir colecciones con separadores, prefix, postfix. buildString proporciona un scope de StringBuilder para construcción concisa. Evita + repetido en loops—usa joinToString o StringBuilder para eficiencia.

kotlin
val parts = listOf("apple", "banana", "cherry")
println(parts.joinToString(", "))  // apple, banana, cherry
println(parts.joinToString(prefix="[", postfix="]"))
val sb = StringBuilder()
for (p in parts) sb.append(p).append(" ")
println(sb.toString().trim())
// buildString for concise building
val result = buildString {
    parts.forEach { append(it).append(";") }
}

String a Número

toInt()/toDouble() lanzan NumberFormatException en entrada inválida. toIntOrNull() retorna null—empareja con ?: para defaults seguros. toBooleanStrict() solo acepta 'true'/'false'. Usa variantes OrNull para entrada de usuario o datos no confiables.

kotlin
val n = "42".toInt()           // 42 (throws if invalid)
val d = "3.14".toDouble()      // 3.14
val safe = "abc".toIntOrNull() // null
val withDefault = "x".toIntOrNull() ?: 0  // 0
val bool = "true".toBooleanStrict()  // true
println(n, d, safe, withDefault)

Regex

La clase Regex envuelve Pattern de Java. find retorna MatchResult? (primer match), findAll retorna todos. matches verifica match completo, containsMatchIn verifica parcial. Usa triple-quoted strings para patrones para evitar double-escaping de backslashes.

kotlin
val email = Regex("[\w.]+@[\w]+\.[a-z]+")
val text = "Contact: [email protected]"
val match = email.find(text)
println(match?.value)  // [email protected]
println(email.matches("[email protected]"))  // true
val replaced = text.replace(Regex("\d+"), "#")
println(email.containsMatchIn(text))  // true
03

Estructuras de Datos

List

listOf crea listas inmutables, mutableListOf para mutables. Las listas son zero-indexed. Usa first()/last() para los extremos (lanzan en vacío), firstOrNull() para seguridad. contains verifica pertenencia. Prefiere listas inmutables a menos que necesites modificar.

kotlin
val nums = listOf(1, 2, 3)  // immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable[0] = 0
println(nums.size)         // 3
println(nums.first())      // 1
println(nums.last())       // 3
println(nums.contains(2))  // true
println(nums.indexOf(2))   // 1

Map

mapOf crea maps inmutables, mutableMapOf para mutables. 'to' infix crea pares. [] access retorna nullable V? (null si no existe). Usa getOrDefault o getValue (lanza si no existe). Itera con destructuring (k, v).

kotlin
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"])          // 30
println(ages.getOrDefault("Eve", 0))  // 0
println(ages.containsKey("Alice"))    // true
for ((k, v) in ages) println("$k: $v")
val mutable = mutableMapOf("a" to 1)
mutable["b"] = 2  // add/update

Set

setOf crea sets inmutables, mutableSetOf para mutables. union/intersect/subtract retornan nuevos sets. Los sets enforce uniqueness—add retorna false si el elemento existe. Usa para deduplicación y operaciones de conjuntos. LinkedHashSet preserva el orden de inserción.

kotlin
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b)        // [1,2,3,4,5]
println(a intersect b)    // [3]
println(a subtract b)     // [1,2]
println(a.contains(2))    // true
val mutable = mutableSetOf(1, 2)
mutable.add(3)  // true if added

Array & Primitive Arrays

arrayOf crea Array<T> (boxed para primitivos). Usa intArrayOf/doubleArrayOf para arrays primitivos unboxed (mejor rendimiento). Array(size) { init } crea con una función init. Los arrays son mutables; prefiere List para la mayoría de casos de uso.

kotlin
val arr = arrayOf(1, 2, 3)  // Array<Int>
arr[0] = 10
println(arr.size)  // 3
// Specialized arrays for primitives (no boxing)
val ints = intArrayOf(1, 2, 3)
val doubles = doubleArrayOf(1.0, 2.0)
// Array constructor
val squares = Array(5) { it * it }  // [0,1,4,9,16]
println(squares.toList())

Pair & Triple

Pair y Triple agrupan 2-3 valores. 'to' infix crea Pairs (común para maps). Destructura con val (a, b) = pair. Para más de 3 valores o campos con nombre, usa data classes—son más claros y mantenibles.

kotlin
val pair = "Alice" to 30  // Pair<String, Int>
println(pair.first)   // Alice
println(pair.second)  // 30
val (name, age) = pair  // destructuring
val triple = Triple(1, "a", 2.0)
println(triple.first, triple.third)
// Useful for returning two values
fun minMax(list: List<Int>): Pair<Int, Int> =
    Pair(list.min(), list.max())
04

Control Flow

If / Else (Expresión)

En Kotlin, if/else es una expresión que retorna un valor. Esto reemplaza al operador ternario. Ambas ramas deben retornar tipos compatibles. Usa para asignación condicional concisa. Como statement (sin else), retorna Unit.

kotlin
val score = 85
val grade = if (score >= 90) "A"
            else if (score >= 80) "B"
            else if (score >= 70) "C"
            else "F"
println(grade)  // B
// if returns a value—no ternary needed
// Both branches must have compatible types

When (Switch)

when es el poderoso switch de Kotlin—soporta literales, ranges (in), negación (!in), múltiples valores (coma) e is-checks. Debe ser exhaustivo cuando se usa como expresión con tipos sealed. Sin subject, es una cadena if/else más limpia.

kotlin
val n = 2
val label = when (n) {
    0 -> "zero"
    1, 2, 3 -> "small"
    in 4..10 -> "medium"
    !in 1..100 -> "out of range"
    else -> "large"
}
println(label)  // small
// When without subject = multi-condition if
when {
    n > 0 -> println("positive")
    n < 0 -> println("negative")
    else -> println("zero")
}

For Loops & Ranges

.. es range inclusivo, until es exclusivo, downTo cuenta hacia abajo, step establece el incremento. withIndex() da iteración indexada. for-in funciona con cualquier Iterable. Estos ranges son concisos y legibles para loops de conteo.

kotlin
for (i in 0 until 5) print(i)  // 01234 (excludes 5)
for (i in 1..5) print(i)      // 12345 (includes 5)
for (i in 5 downTo 1) print(i)  // 54321
for (i in 1..10 step 2) print(i)  // 13579
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

While & Do-While

while verifica antes, do-while verifica después (se ejecuta al menos una vez). Ambos requieren estado mutable. Prefiere for loops u operaciones funcionales (map, filter) para inmutabilidad. Usa while para loops driven por condiciones donde no se conoce el conteo.

kotlin
var count = 0
while (count < 3) {
    println(count)
    count++
}
var j = 0
do {
    println(j)
    j++
} while (j < 3)
// do-while runs at least once
// Prefer for loops when possible

Break & Continue (Labels)

continue salta a la próxima iteración, break sale del loop. Las labels (@outer) habilitan salir de loops anidados—define con label@ antes del loop, break/continue con @label. Usa con moderación; refactorizar a funciones es a menudo más limpio.

kotlin
for (i in 1..5) {
    if (i == 3) continue  // skip 3
    if (i == 5) break     // stop at 5
    println(i)  // 1, 2, 4
}
// Labeled breaks for nested loops
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break@outer
        println("$i,$j")
    }
}
05

Funciones & Lambdas

Definición de Funciones

Las funciones usan fun name(params): ReturnType. Expression body (= expr) para expresiones simples. Default params y named arguments soportados. Unit return type = solo efecto secundario. Los default params reducen la necesidad de overloading.

kotlin
fun add(a: Int, b: Int): Int = a + b  // expression body
fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg
fun log(msg: String): Unit = println(msg)  // Unit = void

Lambdas & Orden Superior

Lambdas: { params -> body }. 'it' es shorthand para un único parámetro. Pasa lambdas a funciones de orden superior como map/filter/reduce. Trailing lambda syntax: si el último param es una función, puede ir fuera (). Esto es Kotlin idiomático.

kotlin
val square: (Int) -> Int = { x -> x * x }
println(square(5))  // 25
val nums = listOf(1, 2, 3)
println(nums.map { it * 2 })      // [2, 4, 6]
println(nums.filter { it > 1 })   // [2, 3]
println(nums.reduce { a, b -> a + b })  // 6
// 'it' is the implicit single parameter

Extension Functions

Las extension functions añaden métodos a clases existentes (incluso de Java) sin herencia o modificación. Son syntactic sugar para funciones estáticas. 'this' se refiere al receiver. Usa para hacer APIs fluent. Se resuelven en tiempo de compilación (sin dynamic dispatch).

kotlin
fun String.shout(): String = this.uppercase() + "!"
fun Int.isEven(): Boolean = this % 2 == 0
println("hello".shout())  // HELLO!
println(4.isEven())       // true
// Extensions don't modify the class—they're resolved statically
// Use to add utility methods to any type

Inline & Infix

inline copia el cuerpo de la función a los call sites (elimina overhead de lambda). Usa para funciones de orden superior en hot paths. infix habilita llamadas de función sin dot/parens (a op b)—usa para DSLs y operaciones legibles como 'to', 'in', 'and'.

kotlin
inline fun measure(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println("Took ${System.currentTimeMillis() - start}ms")
}
measure { println("working...") }
// Infix functions for readable calls
infix fun Int.times(str: String): String = str.repeat(this)
println(3 times "ab")  // ababab

Vararg & Spread

vararg acepta argumentos variables (recogidos en un array). * spreads un array en vararg. Los named params pueden seguir a vararg. Usa para APIs flexibles como listOf(), funciones estilo printf. El spread operator es el equivalente de Kotlin al spread de JS.

kotlin
fun sum(vararg nums: Int): Int = nums.sum()
println(sum(1, 2, 3, 4))  // 10
// Spread operator to pass array
val arr = intArrayOf(1, 2, 3)
println(sum(*arr))  // 6
// Named/default params can follow vararg
fun greet(vararg names: String, greeting: String = "Hi") =
    names.joinToString { "$greeting, $it!" }
println(greet("A", "B"))
06

Clases & OOP

Clase & Constructor

El primary constructor está en el header de la clase. Los parámetros val/var se vuelven properties (con getters/setters). Sin val/var, son solo parámetros de constructor (no accesibles). El bloque init se ejecuta durante la construcción. Kotlin genera getters/setters automáticamente.

kotlin
class Person(val name: String, val age: Int) {
    fun greet(): String = "Hi, I'm $name"
    fun isAdult(): Boolean = age >= 18
}
val p = Person("Alice", 30)
println(p.greet())   // Hi, I'm Alice
println(p.name)      // Alice (val = property)
println(p.isAdult()) // true
// val/var in constructor = auto property

Data Class

data class auto-genera equals, hashCode, toString, copy y componentN (para destructuring). Usa para data holders. Debe tener al menos un parámetro val/var. copy() crea un clon modificado—genial para actualizaciones inmutables. Destructura con val (a, b) = point.

kotlin
data class Point(val x: Int, val y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2)        // true (value equality)
println(p1.copy(x = 5))  // Point(x=5, y=4)
println(p1)              // Point(x=3, y=4)
// Auto: equals, hashCode, toString, copy, componentN
val (x, y) = p1  // destructuring via componentN()

Sealed Class

Las sealed classes restringen los subtipos al mismo archivo/módulo kotlin. Las expresiones when son exhaustivas (el compilador verifica todos los casos). Usa para ADTs (Algebraic Data Types) representando estados finitos. Combinado con when, esto habilita pattern matching seguro y verificado en compilación.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
object Loading : Result()
fun handle(r: Result): String = when (r) {
    is Success -> "Got ${r.value}"
    is Failure -> "Error: ${r.error}"
    Loading -> "Loading..."
}
// when is exhaustive—compiler enforces all cases

Object & Companion

object declara un singleton (una instancia, lazy initialized). companion object dentro de una clase contiene miembros 'static' (accedidos vía ClassName.member). Usa object para singletons, companion para factory methods y constantes. Companion puede implementar interfaces.

kotlin
object Config {  // singleton
    val version = "1.0"
    fun load() = mapOf("key" to "value")
}
println(Config.version)  // 1.0
class User(val name: String) {
    companion object {
        fun create(name: String) = User(name)
        const val MAX = 100
    }
}
val u = User.create("Alice")  // like static method

Herencia & Interface

Las clases son final por defecto—usa 'open' para permitir herencia. 'override' es requerido. Las interfaces pueden tener implementaciones default. Una clase extiende una clase pero implementa múltiples interfaces. Usa abstract para implementación parcial. Prefiere composición sobre herencia.

kotlin
open class Animal(val name: String) {
    open fun speak() = "..."
}
class Dog(name: String) : Animal(name) {
    override fun speak() = "Woof"
}
interface Greetable {
    val name: String
    fun greet(): String  // can have default impl
}
class Person(override val name: String) : Greetable {
    override fun greet() = "Hi, $name"
}
07

Null Safety & Scope Functions

Safe Calls & Elvis

?. safe call (retorna null si el receiver es null), ?: elvis (proporciona un default). Encadena ?. para acceso profundo (user?.address?.city). Usa ?: return/throw para exits tempranos. Estos hacen el manejo de null conciso y seguro—sin checks explícitos de null necesarios.

kotlin
var name: String? = "Alice"
println(name?.length)      // 5
name = null
println(name?.length)      // null
println(name?.length ?: 0) // 0 (elvis: default if null)
println(name?.length ?: return)  // early return
// Safe call chain
val city: String? = user?.address?.city

let (Null Check)

let ejecuta un bloque solo si el valor es non-null. 'it' es el valor non-null dentro del bloque. Usa para operaciones con null guard en lugar de checks if-null. Patrón común: value?.let { ... } para procesamiento seguro. Retorna el resultado del bloque.

kotlin
var name: String? = "Alice"
name?.let {
    println("Name is $it")  // only runs if not null
    println("Length: ${it.length}")  // it is non-null
}
// Common for null-guarded blocks
val result = name?.let {
    process(it)  // it: String (non-null)
} ?: "default"

apply & also

apply configura un object (retorna el object, 'this' receiver)—ideal para builders. also realiza efectos secundarios (retorna el object, 'it' param)—bueno para logging/debugging en cadenas. Ambos retornan el object original, habilitando cadenas fluent.

kotlin
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)  // 'this' = the list
    println("Built $size items")
}
val config = Config().also {
    it.timeout = 30  // 'it' = config
    println("Configured")
}
// apply: configure and return object (this)
// also: side effects, return object (it)

run & with

run ejecuta un bloque en un object (retorna el resultado del bloque, 'this' receiver)—usa para transformar un object. with es como run pero toma el object como parámetro (no chainable en nullables). Usa para agrupar operaciones en el mismo object.

kotlin
val result = "Hello".run {
    length  // 'this' = string, returns last expr
}  // 5
val r2 = with(StringBuilder()) {
    append("a"); append("b")
    toString()  // returns this
}
// run: object.run { } - returns block result
// with: with(obj) { } - returns block result (not chainable)

TakeIf & TakeUnless

takeIf retorna el object si el predicado es true, null en caso contrario. takeUnless es lo opuesto. Usa para filtrado condicional en cadenas—evita checks if separados. Combina con ?: para defaults. Elegante para pipelines de validación.

kotlin
val age = 25
val valid = age.takeIf { it >= 18 }  // 25 (if true)
val invalid = age.takeIf { it < 18 }  // null (if false)
val adult = age.takeUnless { it < 18 }  // 25
// Useful for filtering in chains
val email = getUser()?.email?.takeIf { it.contains("@") }
println(valid, invalid, adult)
08

Colecciones & Funcional

Map / Filter / Fold

map transforma, filter selecciona, reduce/fold agregan. 'it' es el elemento implícito. fold toma una semilla; reduce no (lanza en vacío). Estos son el core del procesamiento funcional de colecciones—usa en lugar de loops para claridad.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
println(nums.map { it * 2 })          // [2,4,6,8,10]
println(nums.filter { it % 2 == 0 })  // [2,4]
println(nums.reduce { a, b -> a + b }) // 15
println(nums.fold(0) { a, b -> a + b }) // 15
println(nums.sum())                   // 15
println(nums.joinToString(", "))      // 1, 2, 3, 4, 5

FlatMap & GroupBy

flatten elimina un nivel de anidamiento. flatMap mapea y aplana en un paso—esencial para transformaciones anidadas. groupBy particiona por una key en un Map. Estos son poderosos para pipelines de procesamiento y análisis de datos.

kotlin
val nested = listOf(listOf(1, 2), listOf(3, 4))
println(nested.flatten())         // [1,2,3,4]
println(nested.flatMap { it.map { n -> n * 2 } })  // [2,4,6,8]
val words = listOf("apple", "bat", "ant")
val byFirst = words.groupBy { it.first() }
// {a=[apple, ant], b=[bat]}
println(byFirst)

Sorting

sorted/sortedDescending ordenan naturalmente. sortedBy/sortedByDescending ordenan por un key selector. Estos retornan nuevas listas (inmutables). Para listas mutables, usa sort/sortBy (in-place). Usa key selectors para ordenar por un campo específico.

kotlin
val nums = listOf(3, 1, 4, 1, 5)
println(nums.sorted())           // [1,1,3,4,5]
println(nums.sortedDescending()) // [5,4,3,1,1]
val people = listOf("Alice" to 30, "Bob" to 25)
val byAge = people.sortedBy { it.second }  // by age
println(byAge)
val byNameDesc = people.sortedByDescending { it.first }
println(byNameDesc)

Sequences (Lazy)

Las sequences son lazy—las operaciones se difieren hasta una operación terminal (toList, sum, count). Evita colecciones intermedias para mejor rendimiento en datos grandes. Usa asSequence() para pipelines multi-step en colecciones grandes. Como Java Streams.

kotlin
val nums = (1..1000000).toList()
// Eager: creates intermediate lists
val eager = nums.filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Lazy: no intermediate collections
val lazy = nums.asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .toList()  // forces evaluation
println(lazy)  // [4, 8, 12, 16, 20]

Partition & Chunked

partition divide en dos listas por un predicado (retorna Pair). chunked divide en listas de tamaño fijo. windowed crea sliding windows. Estos son útiles para batching, pagination y algoritmos de sliding-window. Todos retornan nuevas colecciones.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
val (evens, odds) = nums.partition { it % 2 == 0 }
println(evens)  // [2, 4]
println(odds)   // [1, 3, 5]
val chunked = nums.chunked(2)
println(chunked)  // [[1, 2], [3, 4], [5]]
val windowed = nums.windowed(3)
println(windowed)  // [[1,2,3], [2,3,4], [3,4,5]]
09

Coroutines & Async

Launch (Fire & Forget)

launch inicia una coroutine que no retorna un resultado (fire-and-forget). delay es non-blocking (a diferencia de Thread.sleep). runBlocking puentea código sync/async (usa en main/tests). Las coroutines son ligeras—miles pueden ejecutarse en pocos threads.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")  // after 1s
    }
    println("Hello")  // immediately
    // launch doesn't return a result
    // Use for side-effect coroutines
}
// Output: Hello, then World! after 1s

Async & Await

async inicia una coroutine que retorna un Deferred<T>. await() suspende hasta que el resultado está listo. Inicia múltiples asyncs antes de awaiting para paralelismo. Usa para computaciones concurrentes que producen resultados. Como Promise/Future en otros lenguajes.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val deferred = async {
        delay(1000)
        42  // return value
    }
    val result = deferred.await()
    println(result)  // 42
    // Parallel execution
    val a = async { computeA() }
    val b = async { computeB() }
    println(a.await() + b.await())  // concurrent
}

Suspend Functions

Las suspend functions pueden pausar y reanudar sin bloquear threads. Solo pueden llamarse desde coroutines u otras suspend functions. coroutineScope proporciona un scope estructurado (espera a todos los children). Usa para APIs async—hace el código async parecer sincrónico.

kotlin
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): String {
    delay(500)  // simulates network
    return "User $id"
}
suspend fun fetchAll(): List<String> = coroutineScope {
    val a = async { fetchUser(1) }
    val b = async { fetchUser(2) }
    listOf(a.await(), b.await())
}
// suspend functions can only be called from coroutines

Flow (Cold Stream)

Flow es el cold async stream de Kotlin (como RxJava Observable). Los valores se producen al recolectar. Usa operadores map/filter/reduce. emit produce, collect consume. Ideal para streaming de datos, eventos o APIs paginadas. Los hot streams usan SharedFlow/StateFlow.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)  // produce value
    }
}
fun main() = runBlocking {
    numbers().collect { println(it) }  // 1,2,3,4,5
    // Transform
    numbers().map { it * 2 }.filter { it > 4 }.collect {
        println(it)  // 6, 8, 10
    }
}

Dispatchers & Context

Dispatchers eligen el thread pool: Main para UI, IO para network/file (pool grande), Default para trabajo CPU (cuenta de cores). withContext cambia el contexto dentro de una coroutine. Usa el dispatcher correcto para evitar bloquear UI o starvar thread pools.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch(Dispatchers.Main) { /* UI thread */ }
    launch(Dispatchers.IO) {
        // Network/file I/O (thread pool)
        val data = fetchData()
    }
    launch(Dispatchers.Default) {
        // CPU-intensive work
        val result = heavyCompute()
    }
    // withContext switches dispatcher
    val data = withContext(Dispatchers.IO) { readFile() }
}
10

Error Handling & I/O

Try / Catch / Finally

try/catch/finally es como Java pero try es una expresión que retorna un valor. No hay checked exceptions—todas son unchecked. Catch excepciones específicas para manejo dirigido. Usa finally para limpieza. Prefiere Result o returns nullable para fallos esperados.

kotlin
val result = try {
    "abc".toInt()
} catch (e: NumberFormatException) {
    0  // fallback
} finally {
    println("cleanup")
}
println(result)  // 0
// try is an expression—returns a value
// Kotlin doesn't have checked exceptions

Result Type

Result<T> envuelve éxito o fallo (como Try en Scala). runCatching convierte excepciones a Result. getOrNull/getOrElse para acceso seguro. onSuccess/onFailure para callbacks. Usa para errores esperados en lugar de excepciones—manejo de errores funcional más limpio.

kotlin
fun parse(s: String): Result<Int> =
    runCatching { s.toInt() }
val r = parse("42")
println(r.getOrNull())    // 42
println(r.getOrElse { 0 }) // 42
r.onSuccess { println("OK: $it") }
    .onFailure { println("Err: ${it.message}") }
val r2 = parse("abc")
println(r2.getOrNull())   // null

Excepciones Personalizadas

Las excepciones personalizadas extienden Exception (o una subclase). Pasa un message para debugging. Catch excepciones específicas antes que las genéricas (el orden importa). Usa excepciones para casos verdaderamente excepcionales; para fallos esperados, prefiere Result o returns nullable.

kotlin
class InvalidAgeException(message: String) : Exception(message)
fun setAge(age: Int) {
    if (age < 0) throw InvalidAgeException("Age cannot be negative: $age")
}
try {
    setAge(-5)
} catch (e: InvalidAgeException) {
    println("Caught: ${e.message}")
} catch (e: Exception) {
    println("Other: ${e.message}")
}

File I/O

Kotlin usa File de Java con extension functions. writeText/readText para casos simples. useLines streams líneas (auto-cierra, eficiente en memoria). Para archivos grandes, usa bufferedReader(). Siempre cierra recursos—usa bloque use { } para auto-cierre.

kotlin
import java.io.File
// Write
File("test.txt").writeText("Hello, File!")
// Read
val content = File("test.txt").readText()
println(content)  // Hello, File!
// Append
File("log.txt").appendText("new line\n")
// Line by line
File("test.txt").useLines { lines ->
    lines.forEach { println(it) }
}

JSON (kotlinx.serialization)

kotlinx.serialization es la biblioteca JSON oficial de Kotlin—segura en tiempo de compilación, sin reflection. Anota data classes con @Serializable. encodeToString/decodeFromString para JSON. Usa @SerialName para nombres de key personalizados, @Optional para defaults. Alternativa: Gson/Moshi.

kotlin
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user)
println(json)  // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name)  // Alice
// Requires @Serializable annotation
11

Coroutines Deep Dive

Structured Concurrency

La structured concurrency vincula coroutines child a un parent scope—el parent no completará hasta que todos los children lo hagan, y un fallo de child cancela siblings. Prefiere coroutineScope sobre GlobalScope. Esto previene coroutine leaks y hace la cancelación predecible.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // coroutineScope waits for all children
    coroutineScope {
        launch { delay(1000); println("A") }
        launch { delay(500); println("B") }
    }
    println("Done")  // after both complete
    // If a child fails, parent cancels siblings
    // Never use GlobalScope unless necessary
}

Cancellation & Cooperative Cancellation

La cancelación es cooperativa—las coroutines deben verificarla en puntos de suspensión (delay, yield, await). Las funciones suspending cancelables lanzan CancellationException. Usa ensureActive() o isActive para verificar. Los loops CPU non-suspending no responderán a cancelación a menos que llames yield() o verifiques isActive.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val job = launch {
        repeat(10) { i ->
            delay(300)
            yield()  // explicit suspension point
            println("Working $i")
        }
    }
    delay(700)
    job.cancelAndJoin()  // cancels and waits
    println("Cancelled")
}
// delay/yield check for cancellation
// isActive checks cancellation state

Exception Handling

Las excepciones no manejadas en launch se propagan al parent (cancelando siblings) a menos que se instale un CoroutineExceptionHandler. async difiere excepciones a await(). Usa try/catch alrededor de await. El handler solo funciona para excepciones no capturadas en launch—usa SupervisorJob para aislar fallos.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e ->
        println("Caught: $e")
    }
    val job = launch(handler) {
        throw RuntimeException("Boom!")
    }
    job.join()
    // async exceptions surface on await()
    val deferred = async { throw IllegalStateException() }
    try { deferred.await() } catch (e: Exception) {
        println("Async failed: $e")
    }
}

Supervision (supervisorScope)

supervisorScope (y SupervisorJob) crea un scope donde el fallo de un child NO cancela sus siblings—cada child falla independientemente. Úsalo para operaciones independientes (e.g., múltiples llamadas API independientes). coroutineScope regular cancela siblings en el primer fallo (fail-fast).

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // supervisorScope: child failures don't cancel siblings
    supervisorScope {
        launch { delay(100); throw RuntimeException("A fails") }
        launch { delay(200); println("B still runs") }
    }
    // vs coroutineScope: A's failure cancels B
    // Use SupervisorJob for long-lived services
    val scope = CoroutineScope(SupervisorJob())
}

Channels (estilo CSP)

Los Channels pasan valores entre coroutines (como Go channels). Rendezvous (capacidad 0) sincroniza sender y receiver; buffered permite queueing. send suspende cuando está lleno, receive cuando está vacío. Siempre close() producers. Para fan-out, usa BroadcastChannel o SharedFlow. Prefiere Flow para la mayoría de necesidades de streaming.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
    val channel = Channel<Int>(3)  // buffered capacity 3
    launch {
        for (i in 1..5) {
            channel.send(i)
            println("Sent $i")
        }
        channel.close()
    }
    // receive() blocks until available
    for (x in channel) println("Got $x")
    // Produce-consume pattern
    // Rendezvous (cap 0) syncs sender/receiver
}
12

Flow Deep Dive

Operadores de Flow

Los operadores de Flow son cold—se ejecutan solo al recolectar. map/filter/take son como Stream/Sequence. transform es el más flexible (puede emitir múltiples valores por entrada). Las variantes flatMap manejan flows anidados: concat (secuencial), merge (concurrente), latest (cancela el anterior). Elige basado en necesidades de ordering/concurrencia.

kotlin
import kotlinx.coroutines.flow.*
fun nums() = flow {
    for (i in 1..5) emit(i)
}
suspend fun main() {
    nums().map { it * it }           // 1,4,9,16,25
        .filter { it > 5 }          // 9,16,25
        .take(2)                    // 9,16
        .collect { println(it) }
    // transform: emit multiple values
    nums().transform { x ->
        emit(x)
        emit(x * 10)
    }.collect { println(it) }
    // flatMapConcat / flatMapMerge / flatMapLatest
}

Buffer & Concurrency

buffer desacopla producer y consumer con una cola de capacidad fija—útil cuando las velocidades de producción y consumo difieren. conflate mantiene solo el último valor (descarta intermedios) para updates de UI/estado. collectLatest cancela el collector anterior cuando llega un nuevo valor—ideal para search-as-you-type.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun events() = flow {
    for (i in 1..3) {
        delay(100); emit(i)
    }
}
fun main() = runBlocking {
    // Without buffer: producer & consumer alternate (slow)
    events().buffer()  // decouple producer/consumer
        .collect { delay(200); println(it) }
    // conflate: drop intermediate values
    events().conflate().collect { println(it) }
    // collectLatest: cancel previous collection
    events().collectLatest { delay(200); println(it) }
}

StateFlow & SharedFlow (Hot Streams)

StateFlow es un hot stream que mantiene un único valor up-to-date—úsalo para estado de UI (reemplaza LiveData). Siempre tiene un valor y conflate-a. SharedFlow es un hot broadcast stream (múltiples collectors, sin valor inicial requerido)—úsalo para eventos. SharedFlow con replay=1 se comporta como un event bus cacheado.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    // StateFlow: holds one value, stateful
    val state = MutableStateFlow(0)
    launch {
        state.collect { println("State: $it") }
    }
    state.value = 1  // update synchronously
    state.value = 2
    // SharedFlow: broadcasts to multiple collectors
    val events = MutableSharedFlow<String>()
    launch { events.collect { println("A: $it") } }
    launch { events.collect { println("B: $it") } }
    events.emit("Hello")  // both receive
}

Manejo de Excepciones en Flow

catch maneja excepciones upstream y puede emitir valores fallback. Solo captura excepciones emitidas antes de él en la cadena. retry/retryWhen recolectan el flow de nuevo en fallo (útil para llamadas de red). Para excepciones downstream, envuelve collect en try/catch. Nunca captures dentro del flow builder—deja que se propague.

kotlin
import kotlinx.coroutines.flow.*
fun numbers() = flow {
    emit(1)
    throw RuntimeException("fail")
    emit(2)  // never reached
}
suspend fun main() {
    // catch operator (upstream only)
    numbers().catch { e -> emit(-1) }
        .collect { println(it) }  // 1, -1
    // retry on failure
    numbers().retry(3) { e ->
        println("Retry: $e"); true
    }.collect { println(it) }
    // retryWhen for custom logic
}

flowOn & Context

flowOn cambia el dispatcher para el flow upstream (producer + operadores encima de él). Esto es esencial cuando el flow hace blocking I/O—envuélvelo con flowOn(Dispatchers.IO). El downstream (collect) se ejecuta en el contexto del caller. Múltiples llamadas flowOn crean contextos separados para cada segmento.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun diskFlow() = flow {
    for (i in 1..3) {
        Thread.sleep(100)  // blocking I/O
        emit(i)
    }
}
suspend fun main() {
    // flowOn changes upstream context
    diskFlow().flowOn(Dispatchers.IO)
        .collect { println(it) }
    // Without flowOn, runs on collector's dispatcher
    // flowOn applies to all operators above it
    diskFlow().map { it * 2 }
        .flowOn(Dispatchers.IO)
        .filter { it > 2 }
        .collect { println(it) }
}
13

Sealed Classes & ADTs

Fundamentos de Sealed Classes

Las sealed classes restringen las subclases a un conjunto conocido (mismo archivo/módulo). El compilador conoce todos los tipos posibles, habilitando expresiones when exhaustivas sin else. Ideal para modelar estados finitos (Result, UiState, respuestas de red). Combinadas con data classes, forman Algebraic Data Types.

kotlin
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
fun handle(r: Result<Int>) = when (r) {
    is Result.Success -> println("Got ${r.value}")
    is Result.Failure -> println("Error: ${r.error}")
    Result.Loading -> println("Loading...")
}
// All subclasses defined in same file/module

Sealed Interfaces (Kotlin 1.5+)

Las sealed interfaces (Kotlin 1.5+) extienden sealed a interfaces, permitiendo que una clase implemente múltiples sealed types—más flexible que las sealed classes. La restricción de mismo archivo/mismo módulo aplica. Útil para modelado de dominio donde un tipo pertenece a múltiples categorías.

kotlin
sealed interface Shape {
    fun area(): Double
}
data class Circle(val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}
data class Square(val side: Double) : Shape {
    override fun area() = side * side
}
// Sealed interfaces allow multiple inheritance
sealed interface Clickable { fun click() }
class Button : Shape, Clickable {
    override fun area() = 0.0
    override fun click() = println("Clicked")
}

When Exhaustivo

Cuando se usa como expresión (no statement), el compilador requiere todos los casos sealed—no se necesita else. Añadir una nueva subclase produce errores de compilación en sitios when no manejados, haciendo el refactoring seguro. Esta es la característica killer de las sealed classes para máquinas de estados y rendering de UI.

kotlin
sealed class NetworkState {
    object Loading : NetworkState()
    data class Success(val data: String) : NetworkState()
    data class Error(val message: String) : NetworkState()
}
fun render(state: NetworkState): String = when (state) {
    NetworkState.Loading -> "Spinner"
    is NetworkState.Success -> "Data: ${state.data}"
    is NetworkState.Error -> "Error: ${state.message}"
    // No else needed—compiler enforces all cases
}
// Adding a new subclass causes compile error
// until you handle it

Modelado ADT (Result/Either)

Las sealed classes modelan sum types (Either/Result) para programación railway-oriented. fold maneja ambas ramas. A diferencia de las excepciones, los errores son explícitos en la signatura de tipo. Arrow-kt proporciona Either/Validated más rico. Usa para manejo de errores predecible y componible sin try/catch.

kotlin
sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}
fun <L, R> Either<L, R>.fold(
    ifLeft: (L) -> Unit,
    ifRight: (R) -> Unit
) = when (this) {
    is Either.Left -> ifLeft(value)
    is Either.Right -> ifRight(value)
}
fun divide(a: Int, b: Int): Either<String, Int> =
    if (b == 0) Either.Left("Divide by zero")
    else Either.Right(a / b)

Sealed Classes con Recursión

Las sealed classes pueden ser recursivas, modelando estructuras de árbol (ASTs, JSON, expresiones). El pattern matching con when + recursión las evalúa elegantemente. Este es el uso clásico de ADT funcional—type-safe, exhaustivo y refactor-friendly. Usado en compiladores, parsers y DSLs de configuración.

kotlin
sealed class Expr {
    data class Num(val value: Int) : Expr()
    data class Add(val left: Expr, val right: Expr) : Expr()
    data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Add -> eval(e.left) + eval(e.right)
    is Expr.Mul -> eval(e.left) * eval(e.right)
}
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
fun main() = println(eval(expr))  // 14
14

Delegated Properties

Delegate lazy

lazy difiere la inicialización hasta el primer acceso y cachea el resultado. Por defecto es thread-safe (double-checked locking). Usa para recursos costosos (configs, conexiones DB, singletons) que pueden no ser necesarios. Pasa el modo NONE para contextos single-threaded para evitar overhead de sincronización.

kotlin
val heavyConfig: Config by lazy {
    println("Initializing...")
    loadConfigFromFile()  // runs once, first access
}
fun main() {
    println("Before access")
    println(heavyConfig)  // initializes here
    println(heavyConfig)  // cached, no re-init
}
// lazy is thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// Use LazyThreadSafetyMode.NONE for single-threaded

observable & vetoable

observable dispara un callback después de cada cambio (logging, efectos secundarios). vetoable puede rechazar cambios retornando false (validación). Ambos toman un valor inicial y un lambda. Usa para estado reactivo, validación o disparar updates de UI. Para escenarios más complejos, usa custom delegates.

kotlin
import kotlin.properties.Delegates
var count: Int by Delegates.observable(0) { _, old, new ->
    println("Changed: $old -> $new")
}
var age: Int by Delegates.vetoable(0) { _, old, new ->
    new >= 0  // reject negative values
}
fun main() {
    count = 1   // prints "Changed: 0 -> 1"
    count = 2   // prints "Changed: 1 -> 2"
    age = 25    // accepted
    age = -5    // rejected, stays 25
}

Custom Property Delegate

Los custom delegates implementan getValue (y setValue para vars) vía ReadOnlyProperty/ReadWriteProperty. Encapsulan comportamiento de propiedad reutilizable (validación, formateo, caching, DB mapping). thisRef es el owner, prop es la metadata de la propiedad. Poderoso para frameworks ORM/serialization.

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString : ReadWriteProperty<Any?, String> {
    private var value: String = ""
    override fun getValue(thisRef: Any?, prop: KProperty<*>) = value
    override fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}
class User {
    var name: String by TrimmedString()
}
fun main() {
    val u = User()
    u.name = "  Alice  "
    println(u.name)  // "Alice"
}

Propiedades respaldadas por Map

Delegar a un Map te permite vincular propiedades a keys dinámicas—útil para parsing JSON, configs o filas ORM sin boilerplate. El nombre de la propiedad debe coincidir con la key del map. Usa MutableMap para propiedades escribibles. Así es como kotlinx.serialization y muchos ORMs funcionan bajo el capó.

kotlin
class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
    val email: String? by map
}
fun main() {
    val u = User(mapOf(
        "name" to "Alice",
        "age" to 30,
        "email" to "[email protected]"
    ))
    println(u.name)  // Alice
}
// Mutable version: MutableMap
class MutableUser(map: MutableMap<String, Any?>) {
    var name: String by map
}

notNull & Delegate Singleton

notNull() es como lateinit pero para cualquier tipo (incluyendo primitivos) y funciona con val/var. Lanza si se accede antes de set. Para singletons, prefiere object (eager) o by lazy (lazy). lateinit es solo para vars en clases; el delegate notNull es más flexible pero tiene ligero overhead.

kotlin
import kotlin.properties.Delegates
class Service {
    // Late-init for non-null, set once
    var config: String by Delegates.notNull()
    // Throws before initialization
}
// Singleton via object (no delegate needed)
object Database {
    val connection = connect()
}
// Or lazy singleton
val db: Database by lazy { Database() }
fun main() {
    val s = Service()
    // println(s.config)  // IllegalStateException
    s.config = "prod"
    println(s.config)  // prod
}
15

Construcción de DSL

Type-Safe Builders

Los type-safe builders usan function types con receiver (lambda con receiver) para crear DSLs anidados. El lambda se ejecuta en el contexto del receiver (this), así llamas sus métodos directamente. Así es como funcionan el HTML DSL de Kotlin, los Gradle build scripts y kotlinx.html—declarativos y verificados en tiempo de compilación.

kotlin
class Table {
    private val rows = mutableListOf<Row>()
    fun row(init: Row.() -> Unit) {
        val r = Row(); r.init(); rows.add(r)
    }
    fun build() = rows
}
class Row {
    private val cells = mutableListOf<String>()
    fun cell(text: String) { cells.add(text) }
    fun build() = cells
}
fun table(init: Table.() -> Unit) = Table().apply(init).build()
val t = table {
    row { cell("A"); cell("B") }
    row { cell("C"); cell("D") }
}

@DslMarker (Control de Scope)

@DslMarker previene la fuga implícita del receiver—dentro de un bloque DSL anidado, solo puedes llamar métodos del receiver más interno. Sin él, Kotlin permitiría llamar métodos de receivers externos, llevando a DSLs confusos/buggy. Anota todas las clases DSL con la misma marker annotation.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(b: Body.() -> Unit) { /*...*/ } }
@HtmlDsl
class Body { fun p(text: String) { /*...*/ } }
fun html(init: HTML.() -> Unit) = HTML().apply(init)
// Without @DslMarker, inner 'this' could call outer methods
html {
    body {
        // p() is unambiguous—Body's method
        // body() would be an error (not Body's method)
        p("Hello")
    }
}

Funciones Infix

Las infix functions habilitan sintaxis de lenguaje natural (a to b, 1 until 10). Deben ser member o extension functions con un único parámetro. Usadas extensivamente en DSLs, frameworks de testing y bibliotecas matemáticas. Ejemplos built-in: to (Pair), until/step (ranges) y operaciones de colecciones.

kotlin
infix fun Int.times(str: String) = str.repeat(this)
fun main() {
    println(3 times "ab")  // "ababab"
    // Equivalent to: 3.times("ab")
    // No dot, no parentheses—reads like natural language
}
// Common in testing
infix fun <T> T.shouldEqual(expected: T) =
    assert(this == expected)
fun test() { 5 shouldEqual 5 }
// Also: to, until, step, in, etc.

Operator Overloading

El operator overloading permite que los objects usen +, -, *, [], (), etc. con sintaxis natural. Marca las funciones con operator. Sobrecarga sensatamente—los tipos matemáticos (Vec, Matrix, Money) se benefician; el overloading arbitrario daña la legibilidad. get/set habilitan indexing; invoke hace los objects callable. Los operadores de range (rangeTo, contains) potencian los for-loops.

kotlin
data class Vec(val x: Int, val y: Int) {
    operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
    operator fun minus(o: Vec) = Vec(x - o.x, y - o.y)
    operator fun times(s: Int) = Vec(x * s, y * s)
    operator fun unaryMinus() = Vec(-x, -y)
    operator fun get(i: Int) = when (i) { 0 -> x; 1 -> y; else -> throw IndexOutOfBoundsException() }
}
fun main() {
    val a = Vec(1, 2)
    val b = Vec(3, 4)
    println(a + b)        // Vec(x=4, y=6)
    println(a * 2)        // Vec(x=2, y=4)
    println(-a)           // Vec(x=-1, y=-2)
    println(a[0])         // 1
}

Function Types con Receiver

Los function types con receiver (A.(B) -> C) permiten que los lambdas accedan al receiver como 'this'. Esta es la base de los DSLs de Kotlin y scope functions (apply, run, with). apply retorna el receiver; run retorna el resultado del lambda. Domina esto para construir DSLs internos expresivos y type-safe.

kotlin
// Lambda with receiver: A.(B) -> C
val greet: String.(Int) -> String = { times -> repeat(times) { this + "!" } }
fun main() {
    println("Hello".greet(3))  // Hello!!!Hello!!!Hello!!!
    // 'this' is the String receiver
}
// Build DSLs with receiver
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun add(s: String) { parts.add(s) }
}
fun build(init: StringBuilder.() -> Unit) =
    StringBuilder().apply(init)
val sb = build { add("a"); add("b") }
16

Testing (JUnit 5, MockK, Turbine)

Fundamentos de JUnit 5

JUnit 5 (Jupiter) es el framework de testing estándar de Kotlin. @Test marca los tests; @BeforeEach/@AfterEach ejecutan setup/teardown por test. Usa nombres con backticks para legibilidad. @ParameterizedTest + @ValueSource ejecutan un test con múltiples entradas. assertThrows verifica excepciones. @Disabled salta tests.

kotlin
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    lateinit var calc: Calculator
    @BeforeEach fun setup() { calc = Calculator() }
    @Test fun `add two numbers`() {
        assertEquals(5, calc.add(2, 3))
    }
    @Test fun `divide by zero throws`() {
        assertThrows<ArithmeticException> { calc.divide(10, 0) }
    }
    @ParameterizedTest
    @ValueSource(ints = [1, 2, 3])
    fun `positive numbers`(n: Int) { assertTrue(n > 0) }
    @Disabled("TODO") @Test fun skip() {}
}

MockK (Mocking)

MockK es la biblioteca de mocking idiomática de Kotlin (maneja final classes, coroutines, extension functions). every { } returns/throws stubbea; verify { } verifica llamadas. Usa relaxed = true para saltar el stubbing de métodos void. MockK soporta suspend functions nativamente vía coEvery/coVerify—esencial para testing de coroutines.

kotlin
import io.mockk.*
interface UserService { fun find(id: Int): String?; fun save(name: String): Int }
class Test {
    val mock = mockk<UserService>()
    @Test fun test() {
        every { mock.find(1) } returns "Alice"
        every { mock.find(any()) } returns null
        every { mock.save(any()) } throws RuntimeException("fail")
        verify { mock.find(1) }              // called once
        verify(exactly = 2) { mock.find(any()) }
        confirmVerified(mock)
    }
}
// relaxUnitFun / relaxed = true for void methods

Turbine (Flow Testing)

Turbine es la biblioteca estándar para testear Flow. test { } recolecta en un scope controlado; awaitItem() afirma la próxima emisión, awaitComplete()/awaitError() verifican terminación. Maneja el timing y virtual time correctamente. Combina con runTest para tests de Flow fast, deterministic y coroutine-friendly.

kotlin
import app.cash.turbine.test
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FlowTest {
    @Test fun testFlow() = runTest {
        flowOf(1, 2, 3).test {
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            awaitComplete()
        }
    }
    @Test fun testError() = runTest {
        flow<Int> { throw RuntimeException("x") }.test {
            assertTrue(awaitError() is RuntimeException)
        }
    }
}

Coroutines Test (runTest)

runTest usa virtual time—los delays se saltan instantáneamente, haciendo los tests async fast y deterministic. Usa StandardTestDispatcher + advanceUntilIdle() para control manual. Inyecta un TestDispatcher en código de producción (en lugar de Dispatchers.Main) para testeabilidad. Evita runBlocking en tests—runTest es la forma moderna.

kotlin
import kotlinx.coroutines.test.*
import kotlinx.coroutines.*
import kotlin.test.*
class RepoTest {
    @Test fun fetch() = runTest {
        // Virtual time: delay(1000) completes instantly
        val result = async { delay(1000); "data" }.await()
        assertEquals("data", result)
    }
    @Test fun withDispatcher() = runTest {
        val scheduler = StandardTestDispatcher()
        val scope = CoroutineScope(scheduler)
        // advanceUntilIdle() runs pending coroutines
        scope.launch { /*...*/ }
        scheduler.advanceUntilIdle()
    }
}

Kotest Assertions & Styles

Kotest ofrece aserciones expresivas (shouldBe, shouldContain) y múltiples estilos de spec (StringSpec, FunSpec, BehaviorSpec para BDD). Se integra con property-based testing y tiene matchers ricos. Elígelo sobre JUnit para tests Kotlin-idiomáticos y legibles. Puede ejecutarse junto a JUnit 5.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith
class MyTest : StringSpec({
    "addition works" {
        1 + 1 shouldBe 2
    }
    "list contains" {
        listOf(1, 2, 3) shouldContain 2
    }
    "string prefix" {
        "hello" shouldStartWith "he"
    }
})
// Other styles: FunSpec, BehaviorSpec, ShouldSpec
17

Programación Funcional Deep Dive

Scope Functions (let, run, with, apply, also)

Las scope functions difieren por receiver (this vs it) y valor de retorno (this vs result). apply/also retornan el receiver (chaining, builders); let/run/with retornan el resultado del lambda (transforms). Usa apply para configuración, let para null-checks/transforms, also para efectos secundarios. No abuses—legibilidad primero.

kotlin
data class User(var name: String, var age: Int)
fun main() {
    val u = User("A", 1)
    // apply: configure & return this (builders)
    val configured = u.apply { age = 30 }
    // let: transform, it = receiver, returns lambda result
    val name = u.let { it.name.uppercase() }
    // run: transform, this = receiver, returns result
    val s = u.run { "$name:$age" }
    // with: like run but called as function
    val str = with(u) { "$name:$age" }
    // also: side effects, returns this (logging/chaining)
    u.also { println("Created $it") }
}

Sequences (Lazy Evaluation)

Las sequences evalúan lazy—cada elemento fluye por todo el pipeline antes de que empiece el siguiente (como Java Streams). Esto evita colecciones intermedias y short-circuits (take/find). Usa para datasets grandes o pipelines multi-step. Para listas pequeñas, la List eager es a menudo más rápida (menos overhead). generateSequence construye streams infinitos.

kotlin
fun main() {
    // Eager: List processes each step fully
    val eager = (1..10).toList()
        .map { it * 2 }
        .filter { it > 5 }
    // Lazy: Sequence processes element-by-element
    val lazy = (1..10).asSequence()
        .map { println("map $it"); it * 2 }
        .filter { println("filter $it"); it > 5 }
        .take(2)
        .toList()  // only processes until 2 found
    // Use Sequence for large/pipelined data
    // generateSequence for infinite streams
    val naturals = generateSequence(1) { it + 1 }
}

Inline Functions & reified

inline elimina el overhead de object/allocation de lambda inlining bytecode—crítico para hot loops. También habilita non-local returns (return desde una función outer dentro de un lambda). Los reified type parameters hacen la info de tipo genérico disponible en tiempo de ejecución (T::class, is T), pero requieren inline. Usa para helpers type-safe como filterIsInstance.

kotlin
// inline: copies bytecode at call site (no lambda overhead)
inline fun measure(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}
// non-local return: inline lambdas can return from outer fun
inline fun forEach(list: List<Int>, f: (Int) -> Unit) {
    for (i in list) f(i)
}
// reified: access generic type at runtime
inline fun <reified T> List<*>.filterIsInstance() =
    filter { it is T } as List<T>
val strs = listOf(1, "a", 2, "b").filterIsInstance<String>()

Funciones de Orden Superior

Las funciones de orden superior toman o retornan funciones. Son la columna vertebral del Kotlin funcional—map, filter, fold, compose. Los function types como (Int) -> Int son first-class. compose construye pipelines. Úsalas para abstraer patrones, habilitar reutilización de código y escribir código declarativo. Marca inline para paths críticos de rendimiento.

kotlin
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) result.add(transform(item))
    return result
}
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int =
    { x -> f(g(x)) }
fun main() {
    val double = { x: Int -> x * 2 }
    val inc = { x: Int -> x + 1 }
    val doubleThenInc = compose(inc, double)
    println(doubleThenInc(3))  // 7
    listOf(1, 2, 3).mapTo { it * it }
}

Recursión & tailrec

tailrec convierte funciones tail-recursive (donde la recursión es la última operación) en loops, previniendo stack overflow. La llamada recursiva debe estar en tail position—sin multiplicación/adición pendiente. Usa un parámetro accumulator para hacer funciones tail-recursive. Esencial para loops estilo funcional en entradas grandes.

kotlin
// Regular recursion: stack overflow on large n
fun factorial(n: Int): Long =
    if (n <= 1) 1 else n * factorial(n - 1)
// tailrec: compiler optimizes to a loop (no stack growth)
tailrec fun factorialTail(n: Int, acc: Long = 1): Long =
    if (n <= 1) acc else factorialTail(n - 1, acc * n)
// Fibonacci with tailrec
tailrec fun fib(n: Int, a: Long = 0, b: Long = 1): Long =
    when (n) { 0 -> a; 1 -> b; else -> fib(n - 1, b, a + b) }
fun main() {
    println(factorialTail(10000))  // no stack overflow
}
18

Genéricos & Variance

Clases & Funciones Genéricas

Los genéricos habilitan código type-safe y reutilizable. Las clases usan <T>; las funciones declaran <T> antes del tipo de retorno. Las constraints (T : Entity) restringen el type bound. A diferencia de Java, los genéricos de Kotlin son reified para inline functions y tienen declaration-site variance, haciendo las APIs genéricas más seguras y ergonómicas.

kotlin
class Box<T>(val value: T) {
    fun get(): T = value
}
fun <T> singletonList(item: T): List<T> = listOf(item)
fun main() {
    val intBox = Box(42)        // Box<Int> inferred
    val strBox = Box<String>("hi")
    val nums = singletonList(1)
}
// Generic constraints
class Repository<T : Entity> {
    fun save(item: T) { /* T is Entity subtype */ }
}
interface Entity { val id: Int }

Variance (in/out)

La variance controla las relaciones de subtipo de los genéricos. out (covariante): un Producer<Dog> es un Producer<Animal>—seguro porque solo lees T. in (contravariante): un Sink<Animal> es un Sink<Dog>—seguro porque solo escribes T. Las colecciones mutables son invariantes (read + write). Usa out/in para diseñar APIs seguras y flexibles.

kotlin
// out (covariant): Producer<Sub> is Producer<Super>
interface Source<out T> { fun next(): T }
// in (contravariant): Consumer<Super> is Consumer<Sub>
interface Sink<in T> { fun put(item: T) }
// invariant (default): neither
class MutableList<T> {
    fun add(item: T) {}
    fun get(): T = TODO()
}
open class Animal
class Dog : Animal()
val src: Source<Animal> = Source<Dog>()  // OK (out)
val sink: Sink<Dog> = Sink<Animal>()     // OK (in)

Variance en la Práctica

Los function types usan variance automáticamente: los parámetros son 'in', los tipos de retorno son 'out'. Por eso (Dog) -> Unit es asignable a (Animal) -> Unit. El principio PECS (Producer Extends, Consumer Super) mapea al out/in de Kotlin. Diseña interfaces genéricas con out cuando solo producen T, in cuando solo consumen T.

kotlin
// Function types are variant by nature
// (T) -> R is contravariant in T, covariant in R
val dogHandler: (Dog) -> Unit = { println(it) }
val animalHandler: (Animal) -> Unit = dogHandler  // OK
// Covariant return types
interface Repository<out T> { fun find(id: Int): T }
class DogRepo : Repository<Dog> {
    override fun find(id: Int): Dog = Dog()
}
val repo: Repository<Animal> = DogRepo()  // OK
// PECS rule: Producer extends, Consumer super
// Kotlin: out = producer, in = consumer

Type Projections & Star Projection

Las type projections hacen temporalmente un tipo variante en un use-site. Array<out Any> significa 'Array de algún subtipo de Any, legible'. Array<in Any> significa 'escribible como Any'. Star projection (*) significa 'algún tipo desconocido'—útil cuando solo necesitas size/contains, no el tipo de elemento. Como los wildcards de Java.

kotlin
fun copy(from: Array<out Any>, to: Array<in Any>) {
    // from: only read (covariant projection)
    // to: only write (contravariant projection)
    for (i in from.indices) to[i] = from[i]
}
// Star projection: unknown type, read-only
fun printSize(list: List<*>) {
    println(list.size)  // OK (no type needed)
    // list[0]  // type is Any? — limited use
}
val mixed: List<*> = listOf(1, "a", 3.0)
// Use when type is irrelevant or unknown

Reified Type Parameters

reified preserva la info de tipo genérico en tiempo de ejecución (normalmente erased en JVM). Requiere inline (el tipo se conoce en cada call site). Habilita checks is T, T::class y filterIsInstance. Sin reified, pasarías un parámetro Class<T> manualmente. Limitación: los tipos reified no pueden usarse en funciones non-inline o como type parameters de clase.

kotlin
// reified requires inline
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
    filter { it is T } as List<T>
inline fun <reified T> Any.castTo(): T = this as T
inline fun <reified T> typeOf() = T::class.simpleName
fun main() {
    val list = listOf(1, "a", 2, "b", 3)
    val strs: List<String> = list.filterIsInstance()
    println(strs)  // [a, b]
    println(typeOf<String>())  // String
    // Without reified, T is erased at runtime
    // reified makes T::class and 'is T' available
}
19

Null Safety Deep

Tipos Nullable

Kotlin distingue tipos nullable (T?) y non-null (T) en tiempo de compilación. ?. safe call retorna null si el receiver es null. ?: Elvis proporciona un default. !! lanza NPE (evita). El compilador impone null checks, eliminando NullPointerException en código Kotlin.

kotlin
var name: String = "Alice"  // Non-null
// name = null  // Error
var nickname: String? = null  // Nullable
// Safe call
println(nickname?.length)  // null if nickname is null
// Elvis operator
val len = nickname?.length ?: 0  // 0 if null
// Not-null assertion (use sparingly)
val len2 = nickname!!.length  // NPE if null

let & run

let ejecuta un bloque si el valor es non-null (it = el valor). run llama un bloque con el object como receiver, retorna result. apply configura un object, retorna el object. also realiza efectos secundarios, retorna el object. Estas scope functions reducen null checks y mejoran la legibilidad.

kotlin
val name: String? = "Alice"
name?.let {
    println("Length: ${it.length}")  // Only if not null
}
// run: object + block
val result = "Hello".run {
    length  // Returns 5
}
// apply: configure object
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)
}

lateinit

lateinit difiere la inicialización de propiedades non-null. No puede usarse con primitivos o tipos nullable. Lanza UninitializedPropertyAccessException si se accede antes de init. ::prop.isInitialized verifica. Útil para dependency injection y propiedades gestionadas por lifecycle.

kotlin
class Service {
    lateinit var dependency: Database
    fun init() {
        dependency = connectDatabase()
    }
    fun use() {
        if (::dependency.isInitialized) {
            dependency.query()
        }
    }
}

Colecciones Nullable

Las colecciones pueden contener elementos nullable (List<String?>) o ser nullable (List<String>?). filterNotNull elimina nulls. firstOrNull retorna null en lugar de lanzar. Maneja elementos nullable con safe calls. Sé explícito sobre la nullabilidad en tipos de colección.

kotlin
val list: List<String?> = listOf("a", null, "b")
val filtered = list.filterNotNull()  // ["a", "b"]
val lengths = list.map { it?.length }  // [1, null, 1]
val first = list.firstOrNull()  // "a" or null
val firstNonEmpty = list.firstOrNull { it?.isNotEmpty() == true }

Platform Types

Los platform types surgen de la interoperabilidad con Java donde la nullabilidad es desconocida. Kotlin no puede imponer null safety para ellos. Siempre declara tipos nullable explícitamente para valores de retorno de Java. Usa anotaciones @Nullable/@NotNull en Java. Las anotaciones JSR-305 ayudan a Kotlin a inferir nullabilidad.

kotlin
// Java interop: platform type (no null info)
val name: String = javaObject.getName()  // May be null!
// Kotlin does not know if Java returns null
// Fix: explicit nullable type
val name2: String? = javaObject.getName()
// Or @Nullable annotation in Java
20

Construcción de DSL

Builder con Lambda Receiver

Lambda con receiver (T.() -> Unit) habilita sintaxis DSL. Dentro del lambda, this es el object receiver. Los métodos pueden llamarse sin cualificación. Así es como Kotlin construye DSLs type-safe como builders de Gradle, HTML y SQL.

kotlin
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun line(s: String) { parts.add(s) }
    fun build() = parts.joinToString("\n")
}
fun buildString(init: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.init()  // Execute lambda with sb as receiver
    return sb.build()
}

HTML DSL

HTML DSL usa builders anidados con lambda receivers. Cada tag es una función que crea un child builder. El lambda configura el child. Produce HTML type-safe y componible. kotlinx.html es una implementación real. El mismo patrón funciona para cualquier estructura jerárquica.

kotlin
fun html(init: HTML.() -> Unit): HTML {
    val h = HTML(); h.init(); return h
}
class HTML {
    fun body(init: Body.() -> Unit) { /* ... */ }
}
html {
    body {
        // this: Body
        p("Hello")
    }
}

@DslMarker

@DslMarker previene el acceso implícito al receiver en scopes externos. Sin él, tanto los métodos de HTML como de Body son accesibles, causando confusión. La anotación restringe el acceso al receiver más interno. Hace los DSLs type-safe e inequívocos. Esencial para DSLs complejos.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(...) {} }
@HtmlDsl
class Body { fun p(...) {} }
html {
    body {
        // p()  // OK: in Body scope
        // body()  // Error: in Body scope, not HTML
    }
}

Gradle DSL

Gradle Kotlin DSL usa el mismo patrón builder. plugins, dependencies son funciones con lambda receivers. implementation, testImplementation son funciones de configuración de dependencias. Type-safe: el compilador verifica nombres de funciones y tipos de parámetros. Mucho mejor que Groovy para refactoring.

kotlin
plugins {
    kotlin("jvm") version "1.9.0"
}
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}
// All are function calls with lambda receivers

Anko SQL

Anko (y Exposed) proporcionan DSLs SQL type-safe. Las comparaciones de columnas son tipadas. No se puede comparar una columna string con un int. El DSL genera SQL. Previene SQL injection y errores de tipo. El mismo patrón aplica a cualquier lenguaje específico de dominio.

kotlin
fun users(where: SqlExpressionBuilder.() -> Op<Boolean>) {
    // DSL for SQL queries
}
users {
    (Users.age greater 18) and (Users.name like "A%")
}
// Type-safe SQL: compiler checks column types
21

Colecciones

Operaciones de List

Las colecciones de Kotlin tienen APIs funcionales ricas. map, filter, reduce son estándar. groupBy particiona por key. chunked divide en listas de tamaño fijo. windowed crea sliding windows. Todas retornan nuevas colecciones. Usa asSequence() para evaluación lazy.

kotlin
val list = listOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
val grouped = list.groupBy { it % 2 }  // {0=[2,4], 1=[1,3,5]}
val chunked = list.chunked(2)  // [[1,2],[3,4],[5]]

Sequence

Sequence es una colección lazy (como Java Stream). Las operaciones se evalúan on demand. Sin colecciones intermedias. Eficiente para colecciones grandes con operaciones encadenadas. Usa asSequence() para convertir. Fuerza con toList(), toSet(), etc.

kotlin
val result = (1..1000000).asSequence()
    .map { it * 2 }
    .filter { it > 100 }
    .take(10)
    .toList()
// Lazy: only computes what is needed
// No intermediate collections

Destructuring

Destructuring divide objects en variables. Funciona con Pair, Triple, data classes y entries de Map. Las funciones componentN() lo habilitan. withIndex() empareja index y value. Útil para múltiples valores de retorno e iteración. Las data classes auto-generan componentN.

kotlin
val (a, b) = Pair(1, "hello")
val (x, y, z) = Triple(1, 2, 3)
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}
data class Point(val x: Int, val y: Int)
val (px, py) = Point(1, 2)

Mutable vs Inmutable

Kotlin tiene tanto colecciones mutables (MutableList) como inmutables (List). Prefiere inmutables para seguridad. toList() crea una copia inmutable. toMutableList() crea una copia mutable. Las interfaces inmutables no exponen métodos de mutación, previniendo modificación accidental.

kotlin
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)  // OK
val immutable = listOf(1, 2, 3)
// immutable.add(4)  // Error: no add method
// Convert
val imm = mutable.toList()
val mut = imm.toMutableList()

Associate & Partition

toMap/associate convierten a maps. partition divide en dos listas por predicado. flatten fusiona listas anidadas. flatMap mapea y aplana. Estos reemplazan loops verbosos con expresiones declarativas. Todos retornan nuevas colecciones.

kotlin
val list = listOf("a" to 1, "b" to 2)
val map = list.toMap()  // {a=1, b=2}
val byLength = list.associate { it.first to it.second }
val (evens, odds) = list.partition { it.second % 2 == 0 }
// evens: [(a,1)?] odds: [(b,2)]
val flat = listOf(listOf(1), listOf(2, 3)).flatten()  // [1,2,3]
22

Pitfalls Comunes

== vs ===

Kotlin == llama a equals (igualdad de valor), a diferencia de Java. === verifica igualdad de referencia. Usa == para comparación de valor. === rara vez se necesita. Para Integer, los valores -128 a 127 están cacheados, así que === puede ser true o false. Siempre usa == para valores.

kotlin
val a = Integer(127)
val b = Integer(127)
a == b  // true (value equality, calls equals)
a === b  // true (Integer caches -128 to 127)
val c = Integer(128)
val d = Integer(128)
c == d  // true
c === d  // false (not cached)

Companion Object

Kotlin no tiene miembros static. companion object contiene métodos y constantes 'static'. const val es una verdadera constante en tiempo de compilación. El companion object es una instancia singleton. @JvmStatic hace los métodos llamables como static desde Java. Usa top-level functions para statics verdaderos.

kotlin
class MyClass {
    companion object {
        const val CONSTANT = 42
        fun create() = MyClass()
    }
}
MyClass.CONSTANT  // 42
MyClass.create()  // Static-like access
// companion object is a real object, not static

Data Class Copy

Las data classes auto-generan copy() que crea una copia modificada. El original no cambia (inmutable). Solo cambian los campos especificados. Útil para updates. Combinado con destructuring, las data classes son poderosas para modelar datos. Evita var en data classes para inmutabilidad.

kotlin
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // New instance
// alice is unchanged
// copy uses named arguments for changed fields

Exhaustividad de Sealed Class

Las sealed classes restringen los subtipos al mismo archivo/package. when en sealed classes es exhaustivo: el compilador advierte si falta una rama. No se necesita else si todos los casos están cubiertos. Añadir un nuevo subtipo causa advertencias en todas partes. Ideal para máquinas de estados y results.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
fun handle(r: Result) = when (r) {
    is Success -> r.value
    // Warning: Missing Failure branch
}
// Fix: handle all cases
fun handle(r: Result) = when (r) {
    is Success -> r.value
    is Failure -> 0
}  // No else needed

Extension vs Member

Las member functions siempre tienen precedencia sobre las extension functions con la misma signatura. Las extensiones se resuelven en tiempo de compilación (static dispatch), los members en tiempo de ejecución (dynamic dispatch). Las extensiones no pueden sobrescribir members. Usa extensiones para funciones de utilidad, no para polimorfismo.

kotlin
class Foo {
    fun bar() = "member"  // Member function
}
fun Foo.bar() = "extension"  // Extension function
Foo().bar()  // "member" - members win!
// Extensions are resolved statically
// Members are resolved dynamically (virtual)
23

Kotlin Multiplatform

Código Común

Kotlin Multiplatform (KMP) comparte código entre plataformas. expect/actual declara implementaciones específicas de plataforma. commonMain tiene código compartido. Los source sets específicos de plataforma implementan actual. Comparte lógica de negocio, mantén la UI nativa. Gradle configura los targets.

kotlin
// commonMain/src/Platform.kt
expect fun getPlatformName(): String
// androidMain/src/Platform.kt
actual fun getPlatformName(): String = "Android"
// iosMain/src/Platform.kt
actual fun getPlatformName(): String = "iOS"
// Shared business logic
class Greeting {
    fun greet() = "Hello from ${getPlatformName()}"
}

Módulo Compartido

Los proyectos multiplatform usan el bloque kotlin en Gradle. Define targets (android, ios). commonMain tiene dependencias compartidas. Los source sets de plataforma pueden tener deps específicos de plataforma. iOS usa Kotlin/Native para compilación directa. Comparte lógica, no UI.

kotlin
// build.gradle.kts (shared module)
kotlin {
    androidTarget()
    iosX64(); iosArm64(); iosSimulatorArm64()
    sourceSets {
        val commonMain by getting {
            dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }
        }
    }
}

Networking (Ktor)

Ktor es un cliente HTTP multiplatform. La API es común, el engine es específico de plataforma. Android usa OkHttp/Android engine. iOS usa Darwin. Las coroutines funcionan entre plataformas. Comparte networking, base de datos (SQLDelight) y lógica de negocio.

kotlin
// Shared HTTP client
suspend fun fetchUser(): User {
    return client.get("https://api.example.com/user").body()
}
// Platform-specific engine
// Android: OkHttp or Android
// iOS: Darwin
// Common code uses the same API

SQLDelight

SQLDelight genera Kotlin type-safe desde SQL. Los archivos .sq contienen SQL con named queries. Genera objetos de query type-safe. Funciona entre plataformas (Android, iOS, JVM). SQL es la fuente de verdad. Las migraciones de schema se rastrean. Alternativa a Room para multiplatform.

kotlin
// .sq file: User.sq
CREATE TABLE User (id INTEGER, name TEXT);
selectById: SELECT * FROM User WHERE id = ?;
insertUser: INSERT INTO User (id, name) VALUES (?, ?);
// Generated Kotlin code
val queries: UserQueries = database.userQueries
queries.insertUser(1, "Alice")
val user = queries.selectById(1).executeAsOne()

Compose Multiplatform

Compose Multiplatform extiende Jetpack Compose a iOS, Desktop y Web. Comparte código UI entre plataformas. Misma API @Composable. Entry points específicos de plataforma. Aún experimental para iOS. Reduce la duplicación de UI. Plugin de Gradle: compose-multiplatform.

kotlin
// Shared UI with Compose Multiplatform
@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}
// Android
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { Greeting("World") }
    }
}
// Desktop
fun main() = application {
    Window { Greeting("Desktop") }
}
24

Testing Kotlin

JUnit 5

JUnit 5 es el framework de testing estándar. Kotlin permite nombres de test con backticks para legibilidad. assertEquals, assertThrows son aserciones comunes. @BeforeEach, @AfterEach para setup/teardown. @ParameterizedTest para tests data-driven. Usa kotlin.test para multiplatform.

kotlin
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    @Test
    fun `test addition`() {
        val calc = Calculator()
        assertEquals(5, calc.add(2, 3))
    }
    @Test
    fun `throws on negative`() {
        assertThrows<IllegalArgumentException> { calc.sqrt(-1) }
    }
}

MockK

MockK es una biblioteca de mocking Kotlin-native. Soporta final classes y extension functions. every stubbea, verify verifica llamadas. @MockK crea mocks, @InjectMockKs los inyecta. coEvery/coVerify para coroutines. Mejor soporte de Kotlin que Mockito.

kotlin
class UserServiceTest {
    @MockK
    lateinit var repo: UserRepository
    @InjectMockKs
    lateinit var service: UserService
    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        every { repo.find(1) } returns User("Alice")
    }
    @Test
    fun `test find`() {
        assertEquals("Alice", service.find(1).name)
        verify { repo.find(1) }
    }
}

Kotest

Kotest es un framework de testing Kotlin-first. Múltiples estilos: StringSpec, BehaviorSpec, FunSpec. shouldBe es una aserción fluent. Property testing con Arb. Soporta tests data-driven. Se integra con Spring y Ktor. Más Kotlin-idiomático que JUnit.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
    "addition should work" {
        Calculator().add(2, 3) shouldBe 5
    }
    "should throw on negative" {
        shouldThrow<IllegalArgumentException> { Calculator().sqrt(-1) }
    }
})

Coroutine Testing

runTest de kotlinx-coroutines-test proporciona virtual time. Los delays se saltan instantáneamente. advanceUntilIdle ejecuta coroutines pendientes. Mucho más rápido que tiempo real. Usa TestDispatcher para control fino. La biblioteca Turbine testea emisiones de Flow.

kotlin
@Test
fun `test async`() = runTest {
    val result = fetchData()
    assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantly

Turbine (Flow Testing)

Turbine testea emisiones de Flow. awaitItem obtiene la próxima emisión. awaitComplete afirma completación. awaitError afirma un error. El bloque test suspende hasta que el flow completa. Mucho más limpio que recolectar manualmente. Esencial para testing de Flow.

kotlin
@Test
fun `test flow`() = runTest {
    flowOf(1, 2, 3).test {
        awaitItem() shouldBe 1
        awaitItem() shouldBe 2
        awaitItem() shouldBe 3
        awaitComplete()
    }
}
// Test errors
flow<Int> { throw Exception() }.test {
    awaitError() shouldBe Exception()
}
25

Coroutines Deep Dive

Coroutine Scope

CoroutineScope define un lifetime para coroutines. viewModelScope auto-cancela al clear del VM. SupervisorJob previene que fallos de child cancelen siblings. Los scopes custom necesitan cancelación explícita. Los scopes propagan cancelación a children. Nunca uses GlobalScope en producción (no puede cancelarse).

kotlin
class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = api.getData()
            _data.value = data
        }
    }
}
// Custom scope
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { /* ... */ }
scope.cancel()  // Cancel all children

Dispatchers

Los Dispatchers enrutan coroutines a thread pools. Main: thread de UI (Android). IO: blocking I/O (64+ threads). Default: trabajo CPU (CPU count threads). Unconfined: thread del caller (avanzado). withContext cambia el dispatcher temporalmente. Elegir el dispatcher correcto mejora el rendimiento y previene UI freezes.

kotlin
launch(Dispatchers.Main) { updateUI() }
launch(Dispatchers.IO) { writeFile() }
launch(Dispatchers.Default) { heavyComputation() }
launch(Dispatchers.Unconfined) { runAnywhere() }
// Switch context
withContext(Dispatchers.IO) {
    val data = readDisk()
}

Operadores de Flow

Operadores de Flow: map transforma, filter selecciona, flatMapMerge/flatMapConcat encadenan flows. Los cold flows empiezan fresh por collector. StateFlow mantiene un valor (como LiveData). SharedFlow hace broadcast a múltiples collectors. stateIn convierte cold a hot. buffer/conflate controlan backpressure.

kotlin
flowOf(1, 2, 3, 4, 5)
    .map { it * 2 }
    .filter { it > 4 }
    .collect { println(it) }  // 6, 8, 10

// Cold flow: emits per collector
// StateFlow: hot, stateful
// SharedFlow: hot, broadcast
val state = MutableStateFlow(0)
state.value = 1

Exception Handling

CoroutineExceptionHandler captura excepciones no capturadas en launch. Las excepciones de async se propagan a await. SupervisorJob aísla fallos de child. CancellationException es especial: relanzada, no capturada por catch. Nunca tragues CancellationException. Usa try/finally o use() para limpieza. La cancelación se propaga a través de suspend calls.

kotlin
val handler = CoroutineExceptionHandler { _, e ->
    Log.e("TAG", "Caught: $e", e)
}
scope.launch(handler) {
    throw RuntimeException("oops")
}
// try/catch in coroutine
try {
    riskyCall()
} catch (e: Exception) {
    // handle
}

Channels

Los Channels permiten comunicación coroutine-a-coroutine. send suspende cuando está lleno, receive suspende cuando está vacío. capacity: buffered (RENDEZVOUS=0, UNLIMITED, CONFLATED). close() señala completación. produce crea una coroutine producer. Channel es hot: los valores se consumen una vez. Prefiere Flow para la mayoría de casos de uso.

kotlin
val channel = Channel<Int>(capacity = 10)
launch {
    for (i in 1..5) channel.send(i)
    channel.close()
}
launch {
    for (x in channel) println(x)
}
// Produce pattern
fun numbers() = produce {
    for (i in 1..10) send(i)
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.