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.
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) // trueString 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.
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.
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 nullableConversió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.
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.
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 5Strings
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.
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")) // trueMultiline & 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.
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 whitespaceConstrucció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.
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.
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.
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)) // trueEstructuras 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.
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)) // 1Map
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).
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/updateSet
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.
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 addedArray & 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.
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.
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())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.
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 typesWhen (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.
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.
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.
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 possibleBreak & 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.
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")
}
}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.
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 = voidLambdas & 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.
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 parameterExtension 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).
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 typeInline & 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'.
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") // abababVararg & 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.
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"))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.
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 propertyData 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.
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.
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 casesObject & 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.
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 methodHerencia & 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.
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"
}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.
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?.citylet (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.
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.
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.
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.
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)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.
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, 5FlatMap & 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.
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.
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.
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.
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]]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.
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 1sAsync & 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.
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.
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 coroutinesFlow (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.
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.
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() }
}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.
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 exceptionsResult 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.
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()) // nullExcepciones 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.
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.
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.
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 annotationCoroutines 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.
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.
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 stateException 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.
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).
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.
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
}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.
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.
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.
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.
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.
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) }
}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.
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/moduleSealed 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.
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.
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 itModelado 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.
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.
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)) // 14Delegated 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.
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-threadedobservable & 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.
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.
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ó.
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.
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
}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.
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.
@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.
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.
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.
// 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") }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.
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.
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 methodsTurbine (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.
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.
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.
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, ShouldSpecProgramació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.
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.
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.
// 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.
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.
// 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
}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.
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.
// 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.
// 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 = consumerType 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.
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 unknownReified 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.
// 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
}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.
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 nulllet & 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.