Cheatsheet Kotlin
Linguagem moderna para Android, backend e multiplataforma
Kotlin
Básico e Tipos
Hello World
fun main() {
println("¡Hola, Mundo!")
}
// Sin clase obligatoria (diferente de Java)
// main no necesita args ni tipo de retornoEl punto de entrada es la función main — no necesita clase envolvente ni tipo de retorno declarado. println escribe una línea en stdout. El compilador lo infiere todo automáticamente.
Null safety
var nombre: String? = null // tipo nullable
val len = nombre?.length // safe call (null si null)
val tam = nombre?.length ?: 0 // elvis: default si null
val n = nombre!!.length // fuerza (NPE si null)
// Se ejecuta solo si no es null:
nombre?.let { println(it) }El sistema de tipos elimina NullPointerException en compilación. El ? marca tipos nullable, el ?. encadena llamadas con seguridad y el ?: (elvis) proporciona defaults. let{} ejecuta bloques solo cuando el valor existe.
Operadores
// Aritméticos: + - * / % val r = 10 / 3 // 3 (entero) // Comparación y lógicos: == != > < >= <= && || ! // Range y pertenencia: 1..10 // range 5 in 1..10 // true // Asignación compuesta: += -= *= /= %= // No hay ++/-- prefijo ambiguo
La división entera trunca. El operador in verifica pertenencia a ranges y colecciones. == compara por valor (como equals en Java); === compara referencias. No hay conversiones automáticas en expresiones mixtas.
Variables (val y var)
val nombre = "Kotlin" // inmutable (read-only) var edad = 10 // mutable edad = 11 // OK val pi: Double = 3.14 // tipo explícito // Inferencia de tipo: val x = 42 // Int val s = "texto" // String
Usa val por defecto (inmutable) para evitar bugs; usa var solo cuando el valor deba cambiar. El tipo se infiere a partir del valor, pero puede anotarse explícitamente con : Tipo.
Arrays
val nums = arrayOf(1, 2, 3, 4, 5)
val zeros = IntArray(5) { 0 }
val pares = Array(5) { i -> i * 2 }
println(nums[0]) // 1
println(nums.size) // 5
nums[0] = 10 // mutable
// Iterar:
for (n in nums) println(n)Los arrays tienen tamaño fijo tras la creación. IntArray/DoubleArray son versiones optimizadas sin boxing. La lambda del constructor inicializa cada posición. Para colecciones dinámicas prefiere List/MutableList.
lateinit y lazy
// lateinit: inicialización tardía (var)
class Activity {
lateinit var db: Database
fun onCreate() {
db = Database.abrir()
}
}
// lazy: se calcula en el 1.er acceso (val)
val config: Config by lazy {
Config.cargarDeArchivo()
}lateinit declara propiedades no nulas inicializadas después (ej: inyección de dependencias) — acceder antes lanza excepción. by lazy aplaza el cálculo de un val hasta el primer acceso (thread-safe por defecto).
Tipos de datos
val entero: Int = 42 val largo: Long = 100_000_000L val decimal: Double = 3.14 val flag: Boolean = true val letra: Char = 'A' val texto: String = "Kotlin" val nada: Unit = Unit // Underscore para legibilidad: val millon = 1_000_000
Kotlin trata los primitivos como objetos (sin distinción int/Integer como en Java). El underscore en números mejora la legibilidad. Unit equivale al void de Java pero es un tipo real.
Type aliases
typealias UserId = Long
typealias Callback = (String) -> Unit
typealias Tabla = Map<String, List<Any>>
fun registrar(id: UserId, cb: Callback) {
cb("usuario $id registrado")
}
registrar(42L) { println(it) }typealias crea nombres legibles para tipos complejos — genial para callbacks, IDs y mapas anidados. No crea un tipo nuevo (es solo un atajo), pero mejora mucho la legibilidad de las firmas.
Nothing y Any
// Any: super-tipo de todos
val obj: Any = 42
val obj2: Any = "texto"
// Nothing: función que nunca retorna
fun fallar(msg: String): Nothing {
throw IllegalStateException(msg)
}
// Útil en expresiones:
val x = valor ?: fallar("sin valor")
// aquí x es no-nullAny es el super-tipo de todos los tipos (como Object en Java). Nothing indica que una función nunca retorna normalmente (lanza excepción o bucle infinito) — ayuda al compilador a inferir nullability tras ?:.
Strings y templates
val nombre = "Mundo"
val saludo = "¡Hola, $nombre!"
val template = "2 + 2 = ${2 + 2}"
// Multilínea (triple-quoted):
val multi = """
|Línea 1
|Línea 2
""".trimMargin()
// Sin escape de comillas:
val json = """{"nombre": "$nombre"}"""Los string templates con $ evitan la concatenación; para expresiones usa ${}. Las triple-quoted strings (\"\"\") preservan el formato y son ideales para SQL/JSON — trimMargin() elimina la indentación.
Conversiones de tipos
val x = 42 val s = x.toString() // Int -> String val n = "123".toInt() // String -> Int val d = x.toDouble() // Int -> Double val l = x.toLong() // Int -> Long // Sin conversiones implícitas: // val d: Double = x // ¡ERROR! val d2: Double = x.toDouble() // toIntOrNull (seguro): val inv = "abc".toIntOrNull() // null
Kotlin no hace conversiones numéricas implícitas — usa toInt(), toDouble(), etc. explícitamente. toIntOrNull() devuelve null en vez de lanzar excepción si el string no es un número válido.
Controlo de Fluxo
If como expresión
// if retorna valor:
val max = if (a > b) a else b
// Con bloques:
val msg = if (nota >= 10) {
println("Aprobado")
"OK"
} else {
"Reprobado"
}
// No existe operador ternario ?:
// Usa if-else como expresiónEn Kotlin el if es una expresión que retorna un valor — sustituye al operador ternario (que no existe). El último statement de cada bloque es el valor retornado. Hace el código más declarativo.
While y do-while
var n = 10
while (n > 0) {
n--
}
// do-while se ejecuta al menos 1 vez:
do {
val input = readLine()
println("Leído: $input")
} while (input != "salir")El while verifica la condición antes de ejecutar (puede no ejecutarse nunca). El do-while se ejecuta al menos una vez — ideal para menús y validación de input donde necesitas leer antes de verificar.
Smart casts
fun procesar(obj: Any) {
if (obj is String) {
// obj ya es String aquí:
println(obj.length)
println(obj.uppercase())
}
if (obj !is List<*>) return
// obj es List a partir de aquí:
println(obj.size)
}El compilador hace smart cast automático tras verificaciones is/!is — dentro del bloque, la variable se trata como el tipo testeado sin cast manual. Solo funciona con val locales o propiedades inmutables.
When
val resultado = when (x) {
1 -> "uno"
2, 3 -> "dos o tres"
in 4..10 -> "entre 4 y 10"
!in 20..30 -> "fuera de 20-30"
else -> "otro"
}
// when es expresión (retorna valor)
// else es obligatorio si no es exhaustivoEl when sustituye al switch de Java con patrones más potentes: múltiples valores separados por coma, ranges con in, negación con !in. Es una expresión que retorna valor.
Break, continue y labels
for (i in 1..10) {
if (i == 3) continue // salta el 3
if (i == 7) break // termina en el 7
print(i) // 1 2 4 5 6
}
// Labels para bucles anidados:
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i * j > 4) break@outer
}
}continue salta a la siguiente iteración, break termina el bucle. Los labels (outer@) permiten controlar bucles anidados — break@outer sale directamente del bucle exterior.
return, break en lambdas
fun buscar(lista: List<Int>) {
lista.forEach {
if (it == 3) return // ¡retorna de la función!
}
}
// Label para retornar solo del lambda:
lista.forEach loop@{
if (it == 3) return@loop // solo de este lambda
println(it)
}
// Nombre implícito del lambda:
lista.forEach {
if (it > 5) return@forEach
}Un return dentro de un lambda retorna de la función envolvente (non-local return). Para retornar solo del lambda usa labels (return@forEach). Esto es esencial para controlar el flujo en funciones de orden superior.
When avanzado
// Sin argumento (cadena if-else):
val msg = when {
x < 0 -> "negativo"
x == 0 -> "cero"
else -> "positivo"
}
// Con tipos (smart cast):
fun tipo(obj: Any) = when (obj) {
is String -> "Texto: ${obj.length}"
is Int -> "Número: $obj"
is List<*> -> "Lista: ${obj.size}"
else -> "?"
}El when sin argumento funciona como cadena if-else más legible. Con is, Kotlin hace smart cast automático — dentro del branch, obj ya se trata como el tipo testeado sin cast manual.
Ranges y progresiones
val r1 = 1..10 // IntRange (inclusivo)
val r2 = 1 until 10 // 1..9 (excluye el último)
val r3 = 'a'..'z' // CharRange
val r4 = 10 downTo 1 // decreciente
if (5 in r1) println("contiene")
for (c in 'a'..'f') print(c) // abcdef
val lista = (1..5).toList() // [1,2,3,4,5]Los ranges son objetos Iterable que soportan iteración y verificación de pertenencia (in). until excluye el último elemento (útil para índices). Funcionan con Int, Long y Char.
Bucles for y ranges
for (i in 1..5) print(i) // 1 2 3 4 5
for (i in 5 downTo 1) print(i) // 5 4 3 2 1
for (i in 0..10 step 2) print(i) // 0 2 4 6 8 10
for (i in 1 until 5) print(i) // 1 2 3 4
// Con índice y valor:
for ((i, v) in lista.withIndex()) {
println("$i: $v")
}Los ranges (..) crean secuencias inclusivas. downTo invierte la dirección, step define el incremento y until excluye el último. withIndex() da acceso al índice y al valor simultáneamente.
Expresiones (try/when)
// try como expresión:
val n = try {
"42".toInt()
} catch (e: NumberFormatException) {
0
}
// when como expresión:
val code = when (status) {
"ok" -> 200
else -> 500
}
// if como expresión:
val max = if (a > b) a else bA diferencia de Java, if/when/try son expresiones que retornan valores en Kotlin. Esto elimina variables temporales y hace el código más declarativo. El último statement de cada bloque es el valor.
Funções e Lambdas
Declaración de funciones
fun suma(a: Int, b: Int): Int {
return a + b
}
// Cuerpo de expresión (retorno inferido):
fun doble(x: Int) = x * 2
// Sin retorno útil (Unit):
fun saludar(nombre: String) {
println("¡Hola, $nombre!")
}Las funciones con cuerpo de expresión (=) son más concisas y el tipo de retorno se infiere. El Unit (sin retorno) puede omitirse. Los tipos de los parámetros son siempre obligatorios.
Extension functions
fun String.capitalize(): String =
replaceFirstChar { it.uppercase() }
fun Int.isEven(): Boolean = this % 2 == 0
fun List<Int>.segundo(): Int? =
if (size >= 2) this[1] else null
// Uso:
"kotlin".capitalize() // "Kotlin"
4.isEven() // true
listOf(1, 2).segundo() // 2Las extension functions añaden métodos a clases existentes sin herencia ni modificación del código fuente. Se resuelven estáticamente en compilación. Ideales para utilidades y APIs más expresivas vía fun Tipo.método().
Operator overloading
data class Vec(val x: Int, val y: Int)
operator fun Vec.plus(o: Vec) =
Vec(x + o.x, y + o.y)
operator fun Vec.times(n: Int) =
Vec(x * n, y * n)
val v = Vec(1, 2) + Vec(3, 4) // Vec(4, 6)
val d = Vec(1, 2) * 3 // Vec(3, 6)El operator overloading permite usar +, -, *, [] con tipos personalizados. Basta marcar la función con operator y usar el nombre convencional (plus, times, get).
Default y named args
fun greet(nombre: String, prefijo: String = "Hola") =
"$prefijo, $nombre!"
greet("Ana") // "Hola, Ana!"
greet("Ana", "Buenos días") // "Buenos días, Ana!"
// Named arguments:
fun config(host: String, port: Int = 80, ssl: Boolean = false) {}
config("localhost", ssl = true) // port default 80Los parámetros con default eliminan sobrecargas — la misma función sirve para varias llamadas. Los named args mejoran la legibilidad y permiten saltar parámetros con default, especificando solo los necesarios.
Higher-order functions
// Recibe función:
fun operacion(a: Int, b: Int, op: (Int, Int) -> Int): Int {
return op(a, b)
}
operacion(3, 4) { x, y -> x + y } // 7
// Retorna función:
fun multiplicador(factor: Int): (Int) -> Int =
{ n -> n * factor }
val triple = multiplicador(3)
triple(5) // 15Las higher-order functions reciben o retornan otras funciones — la base de la programación funcional. El tipo (Int, Int) -> Int describe la firma. Permiten crear factories, estrategias y composición de comportamiento.
Varargs y spread
// vararg: N argumentos del mismo tipo
fun suma(vararg nums: Int) = nums.sum()
suma(1, 2, 3, 4, 5) // 15
// Spread operator (*):
val arr = intArrayOf(1, 2, 3)
suma(*arr) // 6
// vararg con otros parámetros:
fun log(nivel: String, vararg msgs: String) {
msgs.forEach { println("[$nivel] $it") }
}
log("INFO", "arrancó", "listo")vararg acepta N argumentos del mismo tipo (internamente es Array). El spread operator (*) pasa un array como vararg. Puede combinarse con parámetros normales usando named args.
Lambdas
val suma = { a: Int, b: Int -> a + b }
val doble: (Int) -> Int = { it * 2 }
val nada: () -> Unit = { println("hola") }
// it: nombre implícito (1 parámetro)
listOf(1, 2, 3).forEach { println(it) }
// Múltiples parámetros:
val mult = { x: Int, y: Int -> x * y }
mult(3, 4) // 12Las lambdas son funciones anónimas de primera clase. it es el nombre implícito cuando hay un solo parámetro. El tipo (Int) -> Int describe la firma. Son la base de las operaciones de colecciones y DSLs.
Scope functions
// let: transforma + null-safety
val len = nombre?.let { it.trim().length }
// apply: configura (retorna el propio objeto)
val p = Persona().apply { nombre = "Ana"; edad = 25 }
// run: bloque con resultado
val info = p.run { "$nombre tiene $edad" }
// also: efecto secundario
lista.also { println("Tam: ${it.size}") }
// with: operaciones en el objeto
with(p) { println(nombre); println(edad) }Las scope functions eliminan variables temporales. let para transformaciones null-safe, apply para configuración/builders, run para computación, also para efectos secundarios y with para múltiples operaciones en el mismo objeto.
Funciones infix y suspend
// infix: llamada sin punto ni paréntesis
infix fun Int.mas(x: Int) = this + x
val r = 5 mas 3 // 8
// "to" es infix (crea Pair):
val par = "a" to 1 // Pair("a", 1)
// suspend: función de coroutine
suspend fun fetchDatos(): String {
delay(1000)
return "datos"
}Las funciones infix (1 parámetro, miembro o extensión) pueden llamarse sin punto ni paréntesis — to es el ejemplo más común. suspend marca funciones que pueden suspenderse en coroutines (solo llamables desde otra suspend o coroutine).
Trailing lambda
// Lambda como último argumento:
listOf(1, 2, 3).forEach({ println(it) })
// Trailing lambda (fuera de los paréntesis):
listOf(1, 2, 3).forEach { println(it) }
// Si es el único argumento:
listOf(1, 2, 3).map { it * 2 }
// Varios lambdas:
fun build(init: Builder.() -> Unit, validate: () -> Boolean) {}
build({ /* config */ }) { /* valida */ }Cuando el lambda es el último argumento, puede ir fuera de los paréntesis (trailing lambda) — esencial para APIs fluidas y DSLs. Si es el único argumento, los paréntesis pueden omitirse por completo.
Inline functions
// inline copia el lambda al call site:
inline fun medirTiempo(bloque: () -> Unit) {
val inicio = System.currentTimeMillis()
bloque()
println("${System.currentTimeMillis() - inicio}ms")
}
// noinline: no hacer inline de este lambda
inline fun foo(a: () -> Unit, noinline b: () -> Unit) {}
// crossinline: prohíbe non-local return
inline fun bar(crossinline b: () -> Unit) {}inline copia el cuerpo del lambda al call site, eliminando la asignación de objetos — crucial en bucles. noinline excluye un lambda específico; crossinline prohíbe non-local returns cuando el lambda se llama en otro contexto.
Classes e Objetos
Clases y constructores
class Persona(val nombre: String, var edad: Int) {
var email: String = ""
init {
require(edad >= 0) { "Edad inválida" }
}
// Constructor secundario:
constructor(nombre: String) : this(nombre, 0)
}
val p = Persona("Ana", 25)El primary constructor en la declaración de la clase es conciso — val/var crean propiedades automáticamente. El bloque init valida en la creación. Los constructores secundarios delegan en el primario con this().
Interfaces
interface Nadador {
fun nadar(): String
// Método con implementación default:
fun flotar(): String = "flotando"
}
interface Volador {
fun volar(): String
}
class Pato : Nadador, Volador {
override fun nadar() = "nadando"
override fun volar() = "volando"
}Las interfaces pueden tener métodos abstractos y métodos con implementación default. Una clase puede implementar múltiples interfaces (pero heredar de una sola clase). Si hay conflicto de defaults, usa super<Interface>.
Companion object
class User private constructor(val nombre: String) {
companion object Factory {
fun create(json: String): User = User(json)
fun guest() = User("Invitado")
const val MAX = 100
}
}
// Acceso vía nombre de la clase:
val u = User.create("{...}")
val g = User.guest()
println(User.MAX)companion object sustituye los miembros static de Java — se accede vía ClassName.miembro(). Ideal para factories, constantes (const val) y métodos utilitarios. Puede tener nombre o ser anónimo.
Propiedades
class Rectangulo(val ancho: Int, val alto: Int) {
// Propiedad calculada:
val area: Int
get() = ancho * alto
// Con setter personalizado:
var nombre: String = ""
set(value) {
field = value.trim()
}
}
val r = Rectangulo(3, 4)
println(r.area) // 12 (calculado)Las propiedades pueden tener get() personalizado (calculadas) y set() con lógica. El field es la backing field (el valor real). No hay métodos getter/setter boilerplate como en Java — todo es propiedad.
Sealed classes
sealed class Resultado<out T> {
data class Exito<T>(val datos: T) : Resultado<T>()
data class Error(val msg: String) : Resultado<Nothing>()
object Cargando : Resultado<Nothing>()
}
fun tratar(r: Resultado<String>) = when (r) {
is Resultado.Exito -> "Datos: ${r.datos}"
is Resultado.Error -> "Error: ${r.msg}"
Resultado.Cargando -> "..."
} // exhaustivo — sin elsesealed class restringe la jerarquía a un conjunto fijo de subclases. El compilador verifica la exhaustividad en el when — si añades un caso nuevo, todos los when rompen en compilación. Ideal para estados y resultados.
Delegation (by)
interface Printer { fun print(msg: String) }
class ConsolePrinter : Printer {
override fun print(msg: String) = println(msg)
}
// Delega toda la interfaz:
class Logger(printer: Printer) : Printer by printer
val log = Logger(ConsolePrinter())
log.print("hola") // imprime en la consolaLa delegation (by) implementa el patrón de delegación sin boilerplate — la clase reenvía todas las llamadas de la interfaz al objeto delegado. Puedes sobrescribir métodos específicos si es necesario.
Data classes
data class User(val nombre: String, val email: String, val edad: Int)
val u1 = User("Ana", "ana@mail.com", 25)
val u2 = u1.copy(nombre = "Maria") // objeto nuevo
println(u1) // User(nombre=Ana, ...)
println(u1 == u2) // false (compara valores)
// Destructuring:
val (nombre, email) = u1data class genera automáticamente equals(), hashCode(), toString(), copy() y componentN(). Ideales para modelos, DTOs y estados. El copy() "modifica" inmutables creando una copia con campos cambiados.
Enum classes
enum class Estado(val color: String) {
ACTIVO("#00FF00"),
INACTIVO("#FF0000"),
PENDIENTE("#FFAA00");
fun descripcion() = "Estado: $name ($color)"
}
val e = Estado.ACTIVO
println(e.color) // #00FF00
println(e.ordinal) // 0
val todos = Estado.values()Los enums en Kotlin son clases completas — pueden tener propiedades, métodos e implementar interfaces. Cada constante es una instancia accedida por Estado.ACTIVO. Útiles para estados y conjuntos fijos; values() lista todos.
Nested e inner classes
class Exterior {
val x = 10
// Nested (sin referencia al exterior):
class Nested {
fun hola() = "nested"
}
// Inner (accede al exterior):
inner class Inner {
fun hola() = "x = $x"
}
}
val n = Exterior.Nested()
val i = Exterior().Inner()Las clases anidadas (nested) no tienen referencia a la instancia exterior (como static en Java). Las clases inner mantienen la referencia y acceden a los miembros del exterior. Usa inner solo cuando realmente necesites el contexto externo.
Herencia (open)
open class Animal(val nombre: String) {
open fun sonido(): String = "..."
}
class Pato(nombre: String) : Animal(nombre) {
override fun sonido() = "¡Quack!"
}
// super:
class Perro(nombre: String) : Animal(nombre) {
override fun sonido() = "¡Guau! " + super.sonido()
}Las clases en Kotlin son final por defecto — usa open para permitir herencia. Los métodos también necesitan open para sobrescribirse con override. El super llama a la implementación de la superclase.
Object (singleton)
// Singleton thread-safe:
object AppConfig {
val apiUrl = "https://api.ejemplo.com"
fun getVersion() = "1.0.0"
}
// Uso:
println(AppConfig.apiUrl)
// Object expression (anónimo):
val listener = object : OnClickListener {
override fun onClick() = println("clicado")
}object crea singletons thread-safe con lazy initialization (una única instancia). Sustituye clases utilitarias con métodos estáticos. Las object expressions crean instancias anónimas de interfaces (como clases anónimas en Java).
Coleções
Listas
val inmutable = listOf(1, 2, 3, 4, 5) val mutable = mutableListOf(1, 2, 3) mutable.add(4) mutable.removeAt(0) mutable[0] = 10 println(inmutable.first()) // 1 println(inmutable.last()) // 5 println(inmutable.size) // 5
listOf() crea listas inmutables (read-only) — la API no expone métodos de modificación. mutableListOf() permite add/remove/set. Prefiere inmutables por defecto para evitar efectos secundarios.
Ordenación
val nums = listOf(5, 2, 8, 1, 9)
val asc = nums.sorted() // [1,2,5,8,9]
val desc = nums.sortedDescending() // [9,8,5,2,1]
// Por propiedad:
val porEdad = personas.sortedBy { it.edad }
val porNombre = personas.sortedByDescending { it.nombre }
// Comparador personalizado:
val custom = nums.sortedWith(compareBy({ it % 2 }, { it }))sorted() devuelve una lista nueva ordenada (no modifica la original). sortedBy ordena por una propiedad extraída. sortedWith acepta un Comparator personalizado. compareBy compone múltiples criterios.
flatMap y flatten
val listas = listOf(listOf(1, 2), listOf(3, 4), listOf(5))
val plana = listas.flatten() // [1,2,3,4,5]
val flat = listas.flatMap { it } // [1,2,3,4,5]
// Transformar y aplanar:
val palabras = frases.flatMap { it.split(" ") }
// Palabras únicas de todas las frases:
val unicas = frases
.flatMap { it.split(" ") }
.distinct()flatMap transforma cada elemento en una colección y lo aplana todo en una única lista. flatten() aplana listas anidadas directamente. Es ideal para extraer y combinar elementos de estructuras anidadas.
Map y Set
val mapa = mapOf("a" to 1, "b" to 2, "c" to 3)
println(mapa["a"]) // 1
println(mapa.getOrDefault("z", 0)) // 0
val mutMap = mutableMapOf<String, Int>()
mutMap["d"] = 4
val set = setOf(1, 2, 2, 3, 3) // {1, 2, 3}
println(3 in set) // trueMap almacena pares clave-valor con acceso O(1) por clave. Set garantiza elementos únicos — los duplicados se ignoran. Ambos tienen versiones mutables. El to crea el Pair usado en la construcción de mapas.
Búsqueda
val nums = listOf(5, 2, 8, 1, 9)
val hallado = nums.find { it > 7 } // 8 (primero)
val último = nums.findLast { it < 5 } // 1
val índice = nums.indexOf(8) // 2
val existe = nums.any { it > 8 } // true
val todos = nums.all { it > 0 } // true
val ninguno = nums.none { it < 0 } // true
val cuenta = nums.count { it % 2 == 0 } // 2find devuelve el primer elemento que satisface el predicado (o null). any/all/none verifican condiciones booleanas. count cuenta los que pasan. indexOf da la posición de un valor.
distinct, chunked y zip
val unicos = listOf(1, 1, 2, 3, 3).distinct() // [1,2,3]
val grupos = (1..7).chunked(3)
// [[1,2,3], [4,5,6], [7]]
val a = listOf("a", "b", "c")
val b = listOf(1, 2, 3)
val pares = a.zip(b) // [(a,1), (b,2), (c,3)]
val (x, y) = listOf(1, 2, 3).partition { it % 2 == 0 }
// x=[2], y=[1,3]distinct() elimina duplicados. chunked(n) divide en sublistas de tamaño fijo (útil para paginación). zip combina dos listas en pares. partition divide en dos listas (pasan/no pasan el predicado).
map y filter
val nums = listOf(1, 2, 3, 4, 5, 6)
val dobles = nums.map { it * 2 }
// [2, 4, 6, 8, 10, 12]
val pares = nums.filter { it % 2 == 0 }
// [2, 4, 6]
val nombres = personas.map { it.nombre }
val mayores = personas.filter { it.edad >= 18 }map transforma cada elemento manteniendo el tamaño; filter selecciona los que pasan el predicado. Ambas devuelven listas nuevas (no modifican la original). Son las operaciones más usadas en colecciones.
Agrupación
val personas = listOf(
Persona("Ana", 25), Persona("Ray", 30), Persona("Mia", 25)
)
val porEdad = personas.groupBy { it.edad }
// {25=[Ana,Mia], 30=[Ray]}
val nombres = personas.associateBy { it.nombre }
// {"Ana"=Persona(...), ...}
val pares = personas.associate { it.nombre to it.edad }
// {"Ana"=25, "Ray"=30, "Mia"=25}groupBy agrupa elementos por clave en un Map<K, List<V>>. associateBy crea un Map indexado por clave. associate construye pares clave-valor arbitrarios. Esenciales para transformar listas en mapas.
joinToString y operaciones de texto
val nums = listOf(1, 2, 3, 4, 5)
val csv = nums.joinToString(", ")
// "1, 2, 3, 4, 5"
val custom = nums.joinToString(
separator = " | ",
prefix = "[",
postfix = "]"
) { it.toString() }
// "[1 | 2 | 3 | 4 | 5]"
val palabras = "a b c".split(" ") // [a, b, c]
val junto = palabras.joinToString("") // "abc"joinToString() convierte colecciones en string con separador, prefijo y postfix personalizables, y un transformador por elemento. split() hace lo inverso. Son ideales para construir CSVs, logs y output formateado.
reduce, fold y sum
val nums = listOf(1, 2, 3, 4, 5)
val suma = nums.reduce { acc, n -> acc + n } // 15
val total = nums.sum() // 15
// fold: con valor inicial (tipos diferentes):
val texto = nums.fold("") { acc, n -> "$acc$n " }
val producto = nums.fold(1) { acc, n -> acc * n } // 120reduce acumula todos los elementos en un único valor (el primero es el inicial). fold acepta un valor inicial explícito y permite tipos diferentes entre acumulador y elementos. sum() es un atajo para sumar números.
Sequences (lazy)
// Eager: crea listas intermedias
val r1 = lista.filter { it > 5 }.map { it * 2 }.take(3)
// Lazy: procesa elemento a elemento
val r2 = lista.asSequence()
.filter { it > 5 }
.map { it * 2 }
.take(3)
.toList() // solo aquí procesa
// Infinita:
val naturales = generateSequence(1) { it + 1 }
val primeros = naturales.take(10).toList()Las Sequences procesan elemento a elemento (lazy) en vez de crear listas intermedias en cada operación. Esencial para colecciones grandes o infinitas — solo computa lo necesario cuando se llama la operación terminal (toList()).
Avançado
Coroutines básico
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000) // suspende sin bloquear
println("Mundo")
}
println("Hola")
}
// Hola (inmediato) -> Mundo (tras 1s)Las coroutines son funciones suspendibles que permiten concurrencia sin threads bloqueados. launch inicia una coroutine fire-and-forget. delay suspende sin bloquear el thread — ideal para I/O, timers y red.
Excepciones en coroutines
val handler = CoroutineExceptionHandler { _, e ->
println("Error: ${e.message}")
}
CoroutineScope(Dispatchers.Default).launch(handler) {
throw RuntimeException("falló")
}
// try/catch con async:
val r = async { operacion() }
try {
r.await()
} catch (e: Exception) {
println("error: $e")
}Los errores en launch se propagan al handler de excepción (CoroutineExceptionHandler). En async, la excepción se relanza en el await() — usa try/catch. Un CoroutineScope cancela los hijos en caso de fallo (structured concurrency).
Reflection
import kotlin.reflect.full.*
class Persona(val nombre: String, val edad: Int)
val p = Persona("Ana", 25)
val klass = p::class
println(klass.simpleName) // "Persona"
klass.memberProperties.forEach {
println("${it.name} = ${it.get(p)}")
}
// Function reference:
val fn = ::println
fn("hola")Reflection (con kotlin-reflect) inspecciona clases, propiedades y funciones en runtime. ::class obtiene el KClass, memberProperties lista las propiedades. Las function references (::println) pasan funciones como valores.
async / await
fun main() = runBlocking {
val r1 = async { fetchDatos1() }
val r2 = async { fetchDatos2() }
// Espera ambos en paralelo:
val total = r1.await() + r2.await()
println("Total: $total")
}
suspend fun fetchDatos1(): Int {
delay(1000); return 42
}async inicia una coroutine que devuelve un Deferred (future). await() suspende hasta que el resultado esté disponible. Múltiples async se ejecutan en paralelo — ideal para llamadas de red concurrentes que reducen el tiempo total.
DSL building
class HtmlBuilder {
val elements = mutableListOf<String>()
fun div(c: String) { elements.add("<div>$c</div>") }
fun p(c: String) { elements.add("<p>$c</p>") }
}
fun html(block: HtmlBuilder.() -> Unit): String {
val b = HtmlBuilder().apply(block)
return b.elements.joinToString("\n")
}
val page = html {
div("Título")
p("Contenido")
}Las DSLs en Kotlin usan extension lambdas (Receiver.() -> Unit) para crear APIs declarativas. El objeto receptor se convierte en el this implícito dentro del bloque. Se usa en Gradle, Ktor, Exposed y Jetpack Compose.
Structured concurrency
fun main() = runBlocking {
// coroutineScope: espera a todos los hijos
coroutineScope {
launch { tarea1() }
launch { tarea2() }
} // solo continúa cuando ambas terminan
// supervisorScope: un fallo no cancela hermanos
supervisorScope {
val a = launch { throw Exception() }
val b = launch { tarea2() } // continúa
}
}Structured concurrency garantiza que las coroutines tienen ciclo de vida definido. coroutineScope espera a todos los hijos y propaga fallos. supervisorScope aísla fallos — que un hijo falle no cancela a los hermanos. Esencial para código robusto.
Dispatchers
import kotlinx.coroutines.*
fun main() = runBlocking {
// CPU-bound:
val r = withContext(Dispatchers.Default) {
calculoPesado()
}
// I/O (red, disco):
val datos = withContext(Dispatchers.IO) {
leerArchivo()
}
// UI (Android):
withContext(Dispatchers.Main) { actualizarUi() }
}Los dispatchers definen en qué thread pool corre la coroutine. Dispatchers.Default para CPU-bound, Dispatchers.IO para I/O (red/disco), Dispatchers.Main para la UI (Android). withContext cambia de dispatcher sin crear nueva coroutine.
Delegates (lazy/observable)
// lazy: calcula 1 vez en el primer acceso
val config: Config by lazy {
Config.cargarDeArchivo()
}
// observable: notifica cambios
var nombre: String by Delegates.observable("") { _, old, new ->
println("$old -> $new")
}
// vetoable: puede rechazar
var edad: Int by Delegates.vetoable(0) { _, _, new ->
new >= 0
}by lazy pospone una computación cara hasta el primer acceso (singleton thread-safe). observable notifica tras cada cambio. vetoable permite rechazar valores inválidos antes de aplicar. Todos eliminan boilerplate de getter/setter.
Channel (comunicación)
import kotlinx.coroutines.channels.Channel
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (i in 1..5) channel.send(i * i)
channel.close()
}
for (valor in channel) {
println(valor) // 1, 4, 9, 16, 25
}
}Channel permite comunicación entre coroutines (productor/consumidor). send() envía (suspende si está lleno), receive() recibe (suspende si está vacío). close() termina el canal. Iterar sobre el channel recibe hasta que se cierre.
Flow (reactivo)
import kotlinx.coroutines.flow.*
fun numeros(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
runBlocking {
numeros()
.filter { it % 2 == 0 }
.map { it * 10 }
.collect { println(it) } // 20, 40
}Flow es la API reactiva de Coroutines — emite múltiples valores asíncronamente. Es cold (solo ejecuta cuando se llama collect). Soporta operadores como filter, map, take — similar a RxJava pero integrado.
Annotations
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Log(val nivel: String = "INFO")
@Log(nivel = "DEBUG")
fun operacion() { }
// Annotations de la stdlib:
@Deprecated("usa nuevo()", ReplaceWith("nuevo()"))
fun antiguo() {}
@JvmStatic
fun utilitario() {}Las annotations añaden metadatos al código. @Target define dónde se aplica, @Retention si está disponible en runtime. La stdlib tiene @Deprecated (con sugerencia de sustitución) y @JvmStatic/@JvmOverloads para interop con Java.
OO e Genéricos
Visibility modifiers
class Ejemplo {
public val a = 1 // público (default)
private val b = 2 // solo en esta clase
protected val c = 3 // clase + subclases
internal val d = 4 // solo en este módulo
}
// Top-level:
private fun helper() {} // solo en este fichero
internal val config = 1 // solo en este móduloKotlin tiene cuatro modificadores: public (default), private (clase/fichero), protected (clase + subclases) e internal (visible en el módulo). Todo es público por defecto — restringe con private lo máximo posible.
reified e inline
// reified mantiene el tipo en runtime:
inline fun <reified T> tipoDe(x: Any): Boolean =
x is T
tipoDe<String>("hola") // true
tipoDe<Int>("hola") // false
// Uso práctico:
inline fun <reified T> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)Normalmente los tipos genéricos se borran en runtime (type erasure). reified (solo con inline) preserva el tipo, permitiendo verificaciones is T y obtener T::class. Muy usado en serialización y reflection.
Value classes (inline)
@JvmInline
value class Email(val valor: String) {
init { require("@" in valor) { "email inválido" } }
fun dominio() = valor.substringAfter("@")
}
val e = Email("ana@mail.com")
println(e.dominio()) // mail.com
// En runtime es solo String (sin overhead de objeto)value class (inline class) envuelve un único valor sin overhead de objeto en runtime — type safety de un tipo distinto con rendimiento de un primitivo. Ideal para IDs, emails y newtypes con validación en el init.
Abstract classes
abstract class Forma {
abstract fun area(): Double
// Método concreto:
fun describir() = "Área: ${area()}"
}
class Círculo(val radio: Double) : Forma() {
override fun area() = Math.PI * radio * radio
}
// No se puede instanciar:
// val f = Forma() // ERRORLas clases abstract no pueden instanciarse y pueden tener métodos abstractos (sin cuerpo) que las subclases implementan. Combinan herencia con contrato — útiles cuando hay código compartido pero comportamiento específico por subtipo.
Type checks y casts
fun procesar(obj: Any) {
if (obj is String) {
println(obj.uppercase()) // smart cast
}
// Cast explícito:
val s = obj as? String // seguro (null si falla)
val n = obj as Int // lanza ClassCastException
// Star projection:
fun tamano(lista: List<*>) = lista.size
}is verifica el tipo con smart cast automático. as? es el cast seguro (devuelve null si falla); as lanza excepción. List<*> (star projection) acepta listas de cualquier tipo cuando no se necesita el tipo concreto.
Builder pattern
class PedidoBuilder {
var item: String = ""
var cantidad: Int = 1
var urgente: Boolean = false
fun build() = Pedido(item, cantidad, urgente)
}
fun pedido(block: PedidoBuilder.() -> Unit): Pedido =
PedidoBuilder().apply(block).build()
val p = pedido {
item = "Libro"
cantidad = 2
urgente = true
}El builder pattern en Kotlin usa extension lambda (Builder.() -> Unit) con apply — crea una DSL declarativa para configurar objetos. Más legible que constructores con muchos parámetros opcionales.
Generics
// Clase genérica:
class Box<T>(val value: T)
val intBox = Box(42) // Box<Int> inferido
val strBox = Box("texto")
// Función genérica:
fun <T> primero(lista: List<T>): T? =
lista.firstOrNull()
// Constraint:
fun <T : Comparable<T>> mayor(a: T, b: T): T =
if (a > b) a else bLos generics permiten código reutilizable con type safety. El compilador infiere tipos en la mayoría de los casos. Los constraints (T : Comparable<T>) restringen los tipos aceptados a quienes implementan una interfaz.
equals y hashCode
class Punto(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Punto) return false
return x == other.x && y == other.y
}
override fun hashCode(): Int = 31 * x + y
}
// ¡data class genera esto automáticamente!
data class Punto2(val x: Int, val y: Int)Sobrescribe equals() y hashCode() juntos para comparación por valor (necesario en Set/Map). === compara referencias. Prefiere data class, que genera ambos automáticamente.
Varianza (in/out)
// out (covarianza): solo produce T
class Productor<out T>(val item: T) {
fun obtener(): T = item
}
val p: Productor<Any> = Productor("texto") // OK
// in (contravarianza): solo consume T
class Consumidor<in T> {
fun consumir(item: T) { println(item) }
}
val c: Consumidor<String> = Consumidor<Any>() // OKout (covarianza) significa que la clase solo produce T — Productor<String> es un Productor<Any>. in (contravarianza) solo consume T. Permite subtipado seguro en genéricos (PECS: Producer Extends, Consumer Super).
Sealed interfaces
sealed interface Evento {
data class Clic(val x: Int, val y: Int) : Evento
data class Tecla(val código: Int) : Evento
object Cierre : Evento
}
fun tratar(e: Evento) = when (e) {
is Evento.Clic -> "clic en ${e.x},${e.y}"
is Evento.Tecla -> "tecla ${e.código}"
Evento.Cierre -> "cierre"
} // exhaustivosealed interface (Kotlin 1.5+) combina interfaces con jerarquía cerrada — las implementaciones se conocen en compilación, permitiendo when exhaustivo. Más flexible que sealed class porque una clase puede implementar varias.
Erros e Null Safety
Try / Catch
// try es expresión:
val n = try {
"42".toInt()
} catch (e: NumberFormatException) {
0
} finally {
println("intento hecho")
}
// Múltiples catch:
try {
operacion()
} catch (e: IllegalArgumentException) {
println("argumento: ${e.message}")
} catch (e: Exception) {
println("otro: $e")
}try es una expresión que devuelve valor — el último statement del bloque try o catch es el resultado. El finally se ejecuta siempre. Puede haber múltiples catch por tipo de excepción.
Safe calls encadenados
// Encadenamiento seguro:
val ciudad = persona?.direccion?.ciudad?.nombre
// Equivalente sin safe call (verboso):
// if (persona != null && persona.direccion != null ...)
// Con let (solo ejecuta si no-null):
persona?.direccion?.let { dir ->
println(dir.ciudad)
enviarCorreo(dir)
}
// Combina con elvis:
val nombre = persona?.nombre ?: "Desconocido"Los safe calls (?.) se encadenan — si cualquier elemento es null, el resultado es null sin NullPointerException. Combinados con let ejecutan bloques solo cuando toda la cadena existe. Sustituye null-checks anidados.
Operador !! (not-null)
var nombre: String? = "Kotlin"
// Fuerza non-null (peligroso):
val len = nombre!!.length
// Si es null -> NullPointerException
// nombre = null
// nombre!!.length // ¡CRASH!
// Prefiere siempre alternativas seguras:
val a = nombre?.length ?: 0 // elvis
val b = nombre?.let { it.length } // letEl operador !! convierte un tipo nullable en non-null, lanzando NullPointerException si es null. Es un "sé lo que hago" — evítalo. Prefiere ?., ?: o let para código verdaderamente null-safe.
Throw y excepciones
fun dividir(a: Int, b: Int): Int {
if (b == 0) {
throw IllegalArgumentException("división por cero")
}
return a / b
}
// throw es expresión (tipo Nothing):
fun validar(x: Int): Int =
if (x < 0) throw IllegalArgumentException("negativo")
else xthrow lanza excepciones (todas son Throwable). En Kotlin no hay checked exceptions — no es obligatorio declarar ni capturar. throw es una expresión de tipo Nothing, útil en expresiones if/elvis.
require y check
// require: valida argumentos (IllegalArgumentException)
fun crearUser(nombre: String, edad: Int) {
require(nombre.isNotBlank()) { "nombre vacío" }
require(edad in 0..150) { "edad inválida: $edad" }
}
// check: valida estado (IllegalStateException)
fun iniciar() {
check(!iniciado) { "ya iniciado" }
iniciado = true
}
// assert (solo en debug):
assert(x > 0) { "x debe ser positivo" }require() valida argumentos de funciones (lanza IllegalArgumentException). check() valida el estado del objeto (lanza IllegalStateException). Ambos aceptan un mensaje lazy. Hacen las precondiciones explícitas y documentadas.
Platform types
// El código Java puede devolver null: // String s = javaLib.getText(); // puede ser null // Kotlin lo trata como platform type (T!): val s = javaLib.getText() // String! // Compila, pero puede dar NPE en runtime // Hazlo explícito: val seguro: String? = javaLib.getText() val len = seguro?.length ?: 0 // @Nullable/@NotNull en Java ayudan
Los platform types (T!) vienen de código Java sin anotaciones de nullability — Kotlin no sabe si pueden ser null. La responsabilidad es del programador. Anota como String? explícitamente y usa safe calls al interactuar con Java.
Custom exceptions
class SaldoInsuficienteException(
val saldo: Double,
val valor: Double
) : Exception("Saldo $saldo insuficiente para $valor")
fun retirar(saldo: Double, valor: Double) {
if (valor > saldo) {
throw SaldoInsuficienteException(saldo, valor)
}
}
try {
retirar(100.0, 150.0)
} catch (e: SaldoInsuficienteException) {
println(e.saldo)
}Crea excepciones personalizadas extendiendo Exception (recuperable) o RuntimeException. Añade propiedades relevantes (como saldo) para contexto. Mantén la jerarquía de excepciones simple y específica.
Result type
val resultado: Result<Int> = runCatching {
"42".toInt()
}
resultado.onSuccess { println("Ok: $it") }
resultado.onFailure { println("Error: $it") }
// Obtener valor:
val n = resultado.getOrDefault(0)
val n2 = resultado.getOrNull()
val n3 = resultado.getOrThrow()
// Encadenar:
val doble = resultado.map { it * 2 }Result<T> encapsula éxito o fallo sin lanzar excepción. runCatching{} captura excepciones en un Result. onSuccess/onFailure tratan cada caso; map transforma el éxito. Alternativa funcional a try/catch.
Operador elvis
// Default si null:
val len = nombre?.length ?: 0
// Con throw:
fun obtenerNombre(): String {
return input?.trim()
?: throw IllegalStateException("sin nombre")
}
// Encadenado:
val valor = a ?: b ?: c ?: "default"
// Con return:
fun procesar(x: Int?) {
val n = x ?: return // early return si null
println(n * 2)
}El operador elvis (?:) proporciona un valor alternativo cuando el lado izquierdo es null. Se combina con throw (lanza si null), return (early exit) y encadenamiento. Es la herramienta central de la null-safety idiomática.
runCatching
// En vez de try/catch:
val valor = runCatching {
api.fetchDatos()
}.getOrElse { error ->
println("Fallo: $error")
datosLocales()
}
// Transformar resultado:
val procesado = runCatching { parse(input) }
.map { it.uppercase() }
.recover { "default" }
.getOrThrow()runCatching{} ejecuta un bloque y captura cualquier excepción en un Result. getOrElse proporciona alternativa en caso de fallo; recover transforma el error en valor. Permite pipelines funcionales sin try/catch verboso.
Ferramentas e Idioms
Idioms esenciales
// Singleton:
object Recurso { }
// Inmutable por defecto:
val lista = listOf(1, 2, 3)
// Expresiones en todo:
val max = if (a > b) a else b
// Data class para modelos:
data class User(val nombre: String)
// Scope functions:
val s = StringBuilder().apply { append("hola") }Idioms Kotlin: prefiere val (inmutable), usa data class para modelos, expresiones en vez de statements, object para singletons y scope functions para eliminar variables temporales. Código conciso y expresivo.
Tests
import kotlin.test.*
class CalculadoraTest {
@Test
fun `suma dos números`() {
val r = suma(2, 3)
assertEquals(5, r)
}
@Test
fun `lanza en división por cero`() {
assertFailsWith<IllegalArgumentException> {
dividir(1, 0)
}
}
@BeforeTest fun setup() { }
@AfterTest fun cleanup() { }
}Kotlin Test (kotlin.test) proporciona aserciones multiplataforma. Los tests pueden tener nombres con espacios entre backticks. assertEquals verifica igualdad, assertFailsWith verifica excepciones. @BeforeTest/@AfterTest hacen setup/cleanup.
Rendimiento y boxing
// Primitivos optimizados:
val arr = IntArray(1000) { it } // sin boxing
val lista = List(1000) { it } // Int? boxed
// inline evita asignación de lambdas:
inline fun repetir(n: Int, bloque: () -> Unit) {
for (i in 0 until n) bloque()
}
// Sequences para colecciones grandes:
val r = datos.asSequence()
.filter { it > 0 }
.map { it * 2 }
.take(10)
.toList()Usa IntArray/DoubleArray para evitar boxing. inline elimina la asignación de objetos lambda en bucles. Las Sequences evitan listas intermedias en cadenas de operaciones. Prefiere val e inmutabilidad para optimizaciones del compilador.
Stdlib útil
// Strings:
"hola".uppercase() // "HOLA"
"a,b,c".split(",") // [a, b, c]
" x ".trim() // "x"
// Números:
val r = (1..10).random()
val n = 3.14159.roundToInt() // 3
// Colecciones:
listOf(1, 2, 3).shuffled()
listOf(1, 2, 3).reversed()
"a" to 1 // PairLa stdlib de Kotlin es rica: uppercase/split/trim para strings, random/roundToInt para números, shuffled/reversed para colecciones. El to crea Pair. Reduce mucho el código utilitario.
Coroutines test
import kotlinx.coroutines.test.*
@Test
fun testAsync() = runTest {
// delay se salta automáticamente:
val resultado = fetchDatos() // suspend
assertEquals("datos", resultado)
}
@Test
fun testConcurrente() = runTest {
val r1 = async { operacion1() }
val r2 = async { operacion2() }
assertEquals(42, r1.await() + r2.await())
}kotlinx-coroutines-test proporciona runTest, que ejecuta coroutines de test de forma determinística — delay se salta (virtual time), haciendo los tests rápidos. async/launch funcionan normalmente dentro del test.
Buenas prácticas
// 1. val por defecto, var solo si es necesario val total = calcular() // 2. Inmutable: listOf en vez de mutableListOf val items = listOf(1, 2, 3) // 3. Expresiones en vez de statements val r = if (x > 0) x else -x // 4. data class para modelos data class Dto(val id: Int, val nombre: String) // 5. Null-safety en vez de !! val n = valor?.length ?: 0
Buenas prácticas: val por defecto, colecciones inmutables, expresiones en vez de statements, data class para modelos y null-safety idiomática (nunca !!). Sigue las Kotlin Coding Conventions oficiales para un estilo consistente.
Interop con Java
// Kotlin llama a Java directamente:
val lista = java.util.ArrayList<String>()
val fecha = java.time.LocalDate.now()
// Java llama a Kotlin:
// (compila a bytecode JVM normal)
// Anotaciones para interop:
@JvmStatic fun util() {} // static real
@JvmOverloads fun f(a: Int, b: Int = 0) {}
@JvmField val x = 1 // campo público
@file:JvmName("Utils") // nombre del archivoKotlin es 100% interoperable con Java — llama a bibliotecas Java directamente y viceversa. Las anotaciones @JvmStatic, @JvmOverloads, @JvmField y @JvmName controlan cómo el código Kotlin aparece para Java.
Multiplatform
// commonMain (compartido):
expect fun plataforma(): String
// androidMain:
actual fun plataforma() = "Android"
// iosMain:
actual fun plataforma() = "iOS"
// jvmMain:
actual fun plataforma() = "JVM"
// Uso en el código común:
fun saludar() = "Hola desde ${plataforma()}"Kotlin Multiplatform comparte lógica de negocio entre plataformas (Android, iOS, JVM, JS, Native). expect declara una API en el código común; actual proporciona la implementación específica de cada plataforma. Ideal para networking, modelos y validación.
Gradle Kotlin DSL
// build.gradle.kts:
plugins {
kotlin("jvm") version "1.9.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(17)
}El Gradle Kotlin DSL (build.gradle.kts) usa Kotlin en vez de Groovy — type-safe, autocompletado en el IDE y refactorizable. plugins{} aplica plugins, dependencies{} gestiona dependencias. Es el estándar recomendado para proyectos nuevos.
Diseño null-safe
// Devuelve no-null siempre que sea posible:
fun nombre(): String = "Ana" // nunca null
// Usa nullable solo cuando sea necesario:
fun encontrar(id: Int): User? =
base.firstOrNull { it.id == id }
// Fuerza el tratamiento en el call site:
val u = encontrar(1)
?: throw NotFoundException("user 1")
// Tipos no-null + elvis = código seguroDiseño null-safe: devuelve tipos no-null por defecto; usa nullable (?) solo cuando la ausencia es un estado válido. Fuerza el tratamiento en el call site con ?: o let. Cuantos menos nullables en la API, más seguro el código.