Anexo 1. Kotlin para Android
- Tipo: Anexo de apoyo — material de consulta y profundización
- Bloque: B1 — Fundamentos: Kotlin, Compose y entorno Android
- RA1 — Aplica tecnologías de desarrollo para dispositivos móviles evaluando sus características y capacidades.
Sobre este anexo#
Este documento es una referencia completa del lenguaje Kotlin orientada al desarrollo Android. Su objetivo es doble: servir como material de consulta durante el curso cuando necesites recordar la sintaxis o el comportamiento de un elemento del lenguaje, y como recurso de profundización para quien quiera ir más allá del repaso acelerado.
Está organizado de forma progresiva: de los conceptos más básicos a los más avanzados. Los apartados marcados con 🔑 son especialmente relevantes para Jetpack Compose o la arquitectura MVVM.
1. El lenguaje Kotlin#
1.1 Historia y posicionamiento#
Kotlin fue desarrollado por JetBrains (creadores de IntelliJ IDEA, base de Android Studio) y presentado en 2011. En 2017, Google lo adoptó como lenguaje oficial de Android. Desde 2019, Google lo recomienda como primera opción (Kotlin-first) para el desarrollo Android, y Jetpack Compose está escrito exclusivamente en Kotlin.
Sus características principales son:
- Concisión: menos código que Java para la misma funcionalidad.
- Seguridad: sistema de tipos con null safety integrado.
- Interoperabilidad: 100 % compatible con Java en ambas direcciones.
- Multiparadigma: combina programación orientada a objetos y funcional.
- Multiplataforma: además de Android, compila a JVM, JavaScript y código nativo.
1.2 Punto de entrada#
Todo programa Kotlin comienza con la función main:
1fun main() {
2 println("Hola, mundo")
3}
4
5// Con argumentos de línea de comandos (menos común en Android)
6fun main(args: Array<String>) {
7 println("Argumentos: ${args.joinToString()}")
8}En Android, el punto de entrada real es la MainActivity o la función anotada con @Composable designada como contenido principal. La función main como tal no se usa en apps Android.
2. Variables y tipos#
2.1 Declaración de variables#
1// val → valor inmutable (no se puede reasignar tras la inicialización)
2val pi: Double = 3.14159
3val appName = "AppFlix" // tipo inferido: String
4
5// var → variable mutable
6var contadorVisitas: Int = 0
7contadorVisitas++
8
9// Declaración sin inicialización (requiere tipo explícito)
10val descripcion: String
11descripcion = "Una aplicación de películas" // se inicializa más adelante, pero solo una vez2.2 Tipos básicos#
Kotlin no tiene tipos primitivos como Java. Todo es un objeto, aunque el compilador los optimiza internamente a primitivos JVM cuando es posible.
| Tipo Kotlin | Equivalente Java | Ejemplo |
|---|---|---|
Int |
int / Integer |
42 |
Long |
long / Long |
42L |
Double |
double / Double |
3.14 |
Float |
float / Float |
3.14f |
Boolean |
boolean / Boolean |
true |
Char |
char / Character |
'A' |
String |
String |
"texto" |
Unit |
void |
retorno vacío |
Any |
Object |
supertipo de todo |
Nothing |
— | función que nunca retorna |
2.3 Conversión de tipos#
Kotlin no hace conversiones de tipos implícitas. Deben ser explícitas:
1val entero: Int = 42
2val largo: Long = entero.toLong() // explícita ✅
3// val largo: Long = entero // ❌ error de compilación
4
5// Conversiones disponibles
6val numero = 3.99
7println(numero.toInt()) // 3 (trunca, no redondea)
8println(numero.toLong()) // 3
9println(numero.toString()) // "3.99"3. Null Safety en profundidad 🔑#
3.1 Tipos nullable y no nullable#
1var nombre: String = "Ana" // no acepta null
2var apodo: String? = null // acepta null
3
4// El compilador garantiza en tiempo de compilación:
5// - 'nombre' nunca será null
6// - 'apodo' puede ser null → obliga a comprobarlo antes de usar3.2 Operadores#
1val texto: String? = obtenerTexto()
2
3// 1. Operador de acceso seguro ?.
4// Si texto es null, devuelve null en lugar de lanzar excepción
5val longitud: Int? = texto?.length
6
7// 2. Operador Elvis ?:
8// Proporciona un valor alternativo cuando la expresión es null
9val longitudSegura: Int = texto?.length ?: 0
10
11// 3. Operador de aserción !! (double bang)
12// Lanza NullPointerException si es null
13// ⚠️ Usar solo cuando tienes garantía absoluta de que no es null
14val longitudForzada: Int = texto!!.length
15
16// 4. Smart cast — el compilador elimina el ? tras comprobar null
17if (texto != null) {
18 println(texto.length) // aquí 'texto' es String, no String?
19}
20
21// 5. let con scope — ejecuta el bloque solo si no es null
22texto?.let { valorNoNulo ->
23 println(valorNoNulo.length)
24 println(valorNoNulo.uppercase())
25}3.3 Casos prácticos#
1data class Usuario(val nombre: String, val email: String?)
2
3fun mostrarEmail(usuario: Usuario) {
4 // Forma idiomática en Kotlin
5 val email = usuario.email ?: "Sin email registrado"
6 println(email)
7}
8
9fun enviarNotificacion(usuario: Usuario) {
10 // let: ejecuta el bloque solo si email no es null
11 usuario.email?.let { email ->
12 println("Enviando notificación a $email")
13 }
14}4. Estructuras de control#
4.1 if como expresión#
En Kotlin, if devuelve un valor (es una expresión, no solo una sentencia):
1val maximo = if (a > b) a else b // equivalente al operador ternario de Java
2
3val mensaje = if (puntuacion >= 5.0) {
4 "Aprobado con ${puntuacion}"
5} else {
6 "Suspenso"
7}4.2 when 🔑#
when es el equivalente de switch en Java, pero mucho más potente:
1// when con valor — comprueba igualdad
2val dia = 3
3val nombreDia = when (dia) {
4 1 -> "Lunes"
5 2 -> "Martes"
6 3, 4 -> "Miércoles o Jueves" // múltiples valores en la misma rama
7 in 5..7 -> "Fin de semana" // rango
8 else -> "Desconocido"
9}
10
11// when sin argumento — condiciones arbitrarias
12val x = 42
13when {
14 x < 0 -> println("Negativo")
15 x == 0 -> println("Cero")
16 x > 100 -> println("Grande")
17 else -> println("Normal")
18}
19
20// when con tipos (especialmente útil con sealed class) 🔑
21sealed class Resultado {
22 data class Exito(val datos: String) : Resultado()
23 data class Error(val codigo: Int) : Resultado()
24 data object Cargando : Resultado()
25}
26
27fun procesar(resultado: Resultado): String = when (resultado) {
28 is Resultado.Exito -> "Datos: ${resultado.datos}"
29 is Resultado.Error -> "Código de error: ${resultado.codigo}"
30 is Resultado.Cargando -> "Cargando..."
31 // Sin 'else': el compilador garantiza que se cubren todos los casos
32}4.3 Bucles#
1// for con rangos
2for (i in 1..5) print("$i ") // 1 2 3 4 5
3for (i in 1 until 5) print("$i ") // 1 2 3 4 (sin incluir 5)
4for (i in 5 downTo 1 step 2) print("$i ") // 5 3 1
5
6// for con colecciones
7val peliculas = listOf("Dune", "Inception")
8for (pelicula in peliculas) println(pelicula)
9
10// withIndex para obtener índice y valor
11for ((indice, pelicula) in peliculas.withIndex()) {
12 println("$indice: $pelicula")
13}
14
15// while y do-while (igual que Java)
16var n = 0
17while (n < 5) n++
18do { n-- } while (n > 0)4.4 Destructuring declarations#
Permite descomponer un objeto en sus componentes:
1data class Punto(val x: Int, val y: Int)
2
3val punto = Punto(3, 7)
4val (x, y) = punto // destructuring
5println("x=$x, y=$y") // x=3, y=7
6
7// Muy útil con Map
8val mapa = mapOf("clave" to "valor")
9for ((clave, valor) in mapa) {
10 println("$clave → $valor")
11}
12
13// Ignorar componentes con _
14val (_, yCoord) = punto // solo nos interesa y5. Funciones en profundidad 🔑#
5.1 Funciones de nivel superior y extensión#
1// Función de extensión — añade funcionalidad a una clase existente sin modificarla
2fun String.esPalindroma(): Boolean {
3 val limpio = this.lowercase().filter { it.isLetter() }
4 return limpio == limpio.reversed()
5}
6
7"Anita lava la tina".esPalindroma() // true
8
9// Extensiones sobre tipos de Android — muy comunes en proyectos reales
10fun Int.dpToPx(density: Float): Float = this * density5.2 Funciones infix#
Las funciones infix se llaman sin punto ni paréntesis, mejorando la legibilidad:
1infix fun Int.sumar(otro: Int): Int = this + otro
2val resultado = 3 sumar 5 // 8
3
4// Ejemplo real: mapOf usa 'to' que es una función infix
5val mapa = mapOf("Android" to 34, "iOS" to 17)
6// equivale a: mapOf(Pair("Android", 34), Pair("iOS", 17))5.3 Funciones de ámbito (Scope functions) 🔑#
Son funciones de extensión que ejecutan un bloque de código sobre un objeto. Son muy usadas en código Android idiomático:
| Función | Receptor en el bloque | Valor de retorno | Uso típico |
|---|---|---|---|
let |
it |
resultado del bloque | transformación o acción si no es null |
run |
this |
resultado del bloque | inicialización con resultado |
with |
this |
resultado del bloque | operaciones múltiples sobre un objeto |
apply |
this |
el propio objeto | configuración/construcción |
also |
it |
el propio objeto | efectos secundarios (logging) |
1// let — transformación con null safety
2val nombre: String? = obtenerNombre()
3val longitud = nombre?.let { it.trim().length } ?: 0
4
5// apply — muy usado para construir objetos
6val lista = mutableListOf<String>().apply {
7 add("Dune")
8 add("Inception")
9 add("Interstellar")
10}
11
12// also — para logging o debugging sin interrumpir la cadena
13val resultado = calcular()
14 .also { println("Resultado intermedio: $it") }
15 .toString()
16
17// run — inicialización compleja
18val config = run {
19 val base = obtenerConfigBase()
20 base.copy(timeout = 30_000)
21}5.4 Funciones inline y reificadas#
1// inline: el compilador incrusta el cuerpo de la función en el punto de llamada
2// Evita la sobrecarga de crear objetos lambda
3inline fun medir(bloque: () -> Unit): Long {
4 val inicio = System.currentTimeMillis()
5 bloque()
6 return System.currentTimeMillis() - inicio
7}
8
9// reified: permite acceder al tipo genérico en tiempo de ejecución
10inline fun <reified T> esInstanciaDe(obj: Any): Boolean = obj is T
11
12esInstanciaDe<String>("Hola") // true
13esInstanciaDe<Int>("Hola") // false6. Clases y objetos en profundidad#
6.1 Constructores#
1// Constructor primario
2class Persona(val nombre: String, var edad: Int)
3
4// Constructor primario con bloque init para validación
5class Persona(val nombre: String, var edad: Int) {
6 init {
7 require(nombre.isNotBlank()) { "El nombre no puede estar vacío" }
8 require(edad >= 0) { "La edad debe ser positiva" }
9 }
10}
11
12// Constructor secundario
13class Persona(val nombre: String) {
14 var edad: Int = 0
15
16 constructor(nombre: String, edad: Int) : this(nombre) {
17 this.edad = edad
18 }
19}
20
21// Mejor práctica: usar parámetros con valor por defecto
22// en lugar de constructores secundarios
23class Persona(val nombre: String, val edad: Int = 0)6.2 Herencia#
En Kotlin, las clases son final por defecto (no se pueden heredar). Se usa open para permitirlo:
1open class Animal(val nombre: String) {
2 open fun sonido(): String = "..."
3}
4
5class Perro(nombre: String) : Animal(nombre) {
6 override fun sonido(): String = "Guau"
7}
8
9// Clases abstractas
10abstract class FormaPDF {
11 abstract fun area(): Double
12 fun descripcion() = "Forma con área ${area()}" // no abstract → tiene implementación
13}6.3 Interfaces#
1interface Reproducible {
2 fun reproducir() // abstracto por defecto
3 fun pausar() { } // con implementación por defecto
4 val duracion: Int // propiedad abstracta
5}
6
7class Cancion(override val duracion: Int) : Reproducible {
8 override fun reproducir() = println("♪ Reproduciendo")
9}
10
11// Kotlin permite implementar múltiples interfaces
12class Podcast(override val duracion: Int) : Reproducible, Compartible {
13 override fun reproducir() = println("▶ Podcast en reproducción")
14 override fun compartir() = println("Compartiendo podcast")
15}6.4 Data class en profundidad 🔑#
1data class Pelicula(
2 val id: Int,
3 val titulo: String,
4 val puntuacion: Double,
5 val generos: List<String> = emptyList()
6)
7
8val p = Pelicula(1, "Dune", 8.0, listOf("Ciencia ficción", "Aventura"))
9
10// Funciones generadas automáticamente:
11println(p.toString()) // Pelicula(id=1, titulo=Dune, ...)
12println(p == p.copy()) // true — equals compara por valor
13val modificada = p.copy(puntuacion = 9.0) // nueva instancia con puntuación modificada
14
15// componentN() — permite destructuring
16val (id, titulo) = p // id = 1, titulo = "Dune"Importante: Las
data classse diseñan para ser inmutables (todos los camposval). Si necesitas modificar un campo, usacopy()en lugar de mutar el objeto.
6.5 Sealed class y sealed interface 🔑#
Las sealed class permiten modelar estados o resultados con una jerarquía cerrada y exhaustiva:
1// Sealed class — todas las subclases deben estar en el mismo paquete
2sealed class EstadoRed {
3 data object Conectado : EstadoRed()
4 data object Desconectado : EstadoRed()
5 data class Error(val codigo: Int, val mensaje: String) : EstadoRed()
6}
7
8// Sealed interface — más flexible: las implementaciones pueden estar en ficheros distintos
9sealed interface Accion {
10 data class NavegaA(val ruta: String) : Accion
11 data class MuestraError(val mensaje: String) : Accion
12 data object Cerrar : Accion
13}
14
15// when exhaustivo sin else
16fun manejar(estado: EstadoRed): String = when (estado) {
17 EstadoRed.Conectado -> "✅ Conectado"
18 EstadoRed.Desconectado -> "❌ Sin conexión"
19 is EstadoRed.Error -> "⚠️ Error ${estado.codigo}: ${estado.mensaje}"
20}6.6 Enum class#
Los enum representan un conjunto fijo de constantes. A diferencia de las sealed class, todas las instancias son del mismo tipo y no pueden tener estados distintos:
1enum class Genero(val etiqueta: String) {
2 ACCION("Acción"),
3 DRAMA("Drama"),
4 COMEDIA("Comedia"),
5 TERROR("Terror");
6
7 fun enEspanyol(): String = etiqueta
8}
9
10val genero = Genero.ACCION
11println(genero.etiqueta) // Acción
12println(Genero.values().size) // 4
13
14// when con enum — también exhaustivo
15when (genero) {
16 Genero.ACCION -> println("🎬 Película de acción")
17 Genero.DRAMA -> println("🎭 Drama")
18 Genero.COMEDIA -> println("😂 Comedia")
19 Genero.TERROR -> println("👻 Terror")
20}¿Cuándo usar
enumy cuándosealed class?
- Usa
enumcuando todas las opciones tienen la misma estructura (solo difieren en el valor de sus propiedades).- Usa
sealed classcuando las diferentes opciones pueden tener estructuras distintas (ej.Errortiene un mensaje,Cargandono tiene nada,Exitotiene datos).
6.7 Object — Singleton#
1object BaseDatosLocal {
2 private val registros = mutableListOf<String>()
3
4 fun insertar(registro: String) {
5 registros.add(registro)
6 }
7
8 fun obtenerTodos(): List<String> = registros.toList()
9}
10
11// Uso: directamente por nombre, sin instanciar
12BaseDatosLocal.insertar("Dune")
13println(BaseDatosLocal.obtenerTodos())
14
15// Object expression (objeto anónimo) — equivalente a clase anónima de Java
16val listener = object : OnClickListener {
17 override fun onClick() = println("Click!")
18}7. Colecciones en profundidad 🔑#
7.1 Tipos de colecciones#
1// ─── LISTAS ───────────────────────────────────────
2val inmutable: List<String> = listOf("a", "b", "c")
3val mutable: MutableList<String> = mutableListOf("a", "b")
4val arrayList: ArrayList<String> = arrayListOf("a")
5
6// Conversiones
7val mutableDesde = inmutable.toMutableList()
8val inmutableDesde = mutable.toList()
9
10// ─── MAPS ─────────────────────────────────────────
11val inmutableMap: Map<String, Int> = mapOf("Ana" to 25, "Luis" to 30)
12val mutableMap: MutableMap<String, Int> = mutableMapOf("Ana" to 25)
13
14mutableMap["Carlos"] = 28 // añadir o actualizar
15mutableMap.getOrDefault("Eva", 0) // valor por defecto si no existe
16
17// ─── SETS ─────────────────────────────────────────
18val inmutableSet: Set<Int> = setOf(1, 2, 3, 2) // → {1, 2, 3}
19val mutableSet: MutableSet<Int> = mutableSetOf(1, 2, 3)7.2 Operaciones funcionales completas#
1val peliculas = listOf(
2 Pelicula(1, "Dune", 8.0),
3 Pelicula(2, "Inception", 8.8),
4 Pelicula(3, "Morbius", 5.2),
5 Pelicula(4, "Interstellar", 8.6)
6)
7
8// filter — filtra por condición
9val buenas = peliculas.filter { it.puntuacion >= 8.0 }
10// [Dune, Inception, Interstellar]
11
12// map — transforma cada elemento
13val titulos = peliculas.map { it.titulo }
14// ["Dune", "Inception", "Morbius", "Interstellar"]
15
16// mapNotNull — transforma y elimina nulls
17val titulosLargos = peliculas.mapNotNull {
18 if (it.titulo.length > 5) it.titulo else null
19}
20
21// sortedBy / sortedByDescending
22val porPuntuacion = peliculas.sortedByDescending { it.puntuacion }
23
24// groupBy — agrupa en un Map
25val porDecena = peliculas.groupBy { (it.puntuacion * 10).toInt() }
26// { 80: [Dune], 88: [Inception], 52: [Morbius], 86: [Interstellar] }
27
28// first / firstOrNull — primer elemento que cumple la condición
29val primeraButna = peliculas.firstOrNull { it.puntuacion >= 9.0 }
30// null — ninguna supera 9.0
31
32// any / all / none
33val hayMala = peliculas.any { it.puntuacion < 6.0 } // true
34val todasBuenas = peliculas.all { it.puntuacion >= 8.0 } // false
35val ningunaRota = peliculas.none { it.puntuacion == 0.0 } // true
36
37// count
38val numBuenas = peliculas.count { it.puntuacion >= 8.0 } // 3
39
40// reduce / fold — acumuladores
41val sumaPuntuaciones = peliculas.sumOf { it.puntuacion } // 30.6
42val media = sumaPuntuaciones / peliculas.size
43
44// flatMap — aplana listas de listas
45val tags = listOf(listOf("acción", "aventura"), listOf("drama"), listOf("thriller"))
46val todosLosTags = tags.flatMap { it } // ["acción", "aventura", "drama", "thriller"]
47
48// zip — combina dos listas
49val nombres = listOf("Ana", "Luis")
50val edades = listOf(25, 30)
51val combinada = nombres.zip(edades) // [(Ana, 25), (Luis, 30)]
52
53// distinct — elimina duplicados
54val conDuplicados = listOf(1, 2, 2, 3, 3, 3)
55val sinDuplicados = conDuplicados.distinct() // [1, 2, 3]
56
57// Encadenamiento de operaciones (muy idiomático en Kotlin)
58val resultado = peliculas
59 .filter { it.puntuacion >= 8.0 }
60 .sortedByDescending { it.puntuacion }
61 .take(3)
62 .map { it.titulo }7.3 Sequences (evaluación perezosa)#
Para colecciones grandes, asSequence() evita crear colecciones intermedias:
1// Sin sequence: crea una lista intermedia para cada operación
2val resultado1 = (1..1_000_000)
3 .filter { it % 2 == 0 } // crea lista de 500.000 elementos
4 .map { it * 2 } // crea otra lista de 500.000 elementos
5 .take(5)
6
7// Con sequence: evaluación perezosa — solo procesa lo necesario
8val resultado2 = (1..1_000_000)
9 .asSequence()
10 .filter { it % 2 == 0 }
11 .map { it * 2 }
12 .take(5)
13 .toList() // materializa el resultado8. Corrutinas en profundidad 🔑#
Las corrutinas son una característica del lenguaje que permite escribir código asíncrono de forma secuencial, sin callbacks ni gestión manual de hilos.
8.1 Conceptos fundamentales#
1// suspend — función que puede suspenderse sin bloquear el hilo
2// Solo puede llamarse desde una corrutina u otra función suspend
3suspend fun cargarDatosRed(): List<String> {
4 delay(2000) // suspende la corrutina 2 segundos, sin bloquear el hilo
5 return listOf("dato1", "dato2")
6}
7
8// Coroutine builders — crean corrutinas
9// launch: inicia una corrutina que no devuelve resultado
10// async: inicia una corrutina que devuelve un Deferred<T> (similar a Future)
11// runBlocking: bloquea el hilo actual hasta que la corrutina termina (solo en tests/main)8.2 Dispatchers — control del hilo#
1import kotlinx.coroutines.*
2
3viewModelScope.launch {
4 // Dispatchers.Main — hilo principal de UI (por defecto en viewModelScope)
5 actualizarUI("Cargando...")
6
7 // withContext — cambia el dispatcher para un bloque específico
8 val datos = withContext(Dispatchers.IO) {
9 // Dispatchers.IO — optimizado para operaciones de entrada/salida
10 // (red, ficheros, base de datos)
11 cargarDatosRed()
12 }
13
14 // Volvemos automáticamente al dispatcher original (Main)
15 actualizarUI("Datos: $datos")
16}| Dispatcher | Uso | Hilos |
|---|---|---|
Dispatchers.Main |
UI, observar StateFlow | Hilo principal |
Dispatchers.IO |
Red, base de datos, ficheros | Pool de hasta 64 hilos |
Dispatchers.Default |
CPU intensivo (ordenación, parseo) | Número de CPUs disponibles |
Dispatchers.Unconfined |
Pruebas (no para producción) | — |
8.3 Flow — streams reactivos 🔑#
Flow<T> es un stream asíncrono de valores. ROOM devuelve Flow<List<T>> para que la UI se actualice automáticamente cuando cambian los datos.
1import kotlinx.coroutines.flow.*
2
3// Crear un Flow
4fun numerosConRetraso(): Flow<Int> = flow {
5 for (i in 1..5) {
6 delay(500)
7 emit(i) // emite un valor al collector
8 }
9}
10
11// Consumir un Flow (desde una corrutina)
12viewModelScope.launch {
13 numerosConRetraso()
14 .filter { it % 2 == 0 }
15 .map { it * 10 }
16 .collect { valor -> println(valor) }
17 // Imprime: 20, 40 (con 500ms de retraso entre cada uno)
18}
19
20// StateFlow — Flow que mantiene el último valor emitido (como LiveData)
21val _estado = MutableStateFlow<String>("inicial")
22val estado: StateFlow<String> = _estado.asStateFlow()
23
24// En Compose, collectAsState() convierte un StateFlow en State
25val estadoCompose by viewModel.estado.collectAsState()8.4 Gestión de errores#
1viewModelScope.launch {
2 try {
3 val datos = withContext(Dispatchers.IO) {
4 servicio.cargarPeliculas()
5 }
6 _estado.value = EstadoUI.Exito(datos)
7 } catch (e: IOException) {
8 // Error de red
9 _estado.value = EstadoUI.Error("Sin conexión a internet")
10 } catch (e: HttpException) {
11 // Error HTTP (4xx, 5xx)
12 _estado.value = EstadoUI.Error("Error del servidor: ${e.code()}")
13 } catch (e: Exception) {
14 // Cualquier otro error
15 _estado.value = EstadoUI.Error(e.message ?: "Error desconocido")
16 }
17}9. Genéricos#
1// Función genérica
2fun <T> primero(lista: List<T>): T? = lista.firstOrNull()
3
4primero(listOf(1, 2, 3)) // 1: Int
5primero(listOf("a", "b", "c")) // "a": String
6
7// Clase genérica
8class Resultado<T>(val valor: T?, val error: String? = null) {
9 val esExito: Boolean get() = valor != null
10}
11
12// Covarianza (out) y contravarianza (in)
13// out T → el tipo solo se produce (devuelto), no se consume → lectura
14interface Productor<out T> {
15 fun producir(): T
16}
17
18// in T → el tipo solo se consume (recibido), no se produce → escritura
19interface Consumidor<in T> {
20 fun consumir(item: T)
21}10. Delegación de propiedades#
1import kotlin.properties.Delegates
2
3// by lazy — inicialización diferida: solo se ejecuta la primera vez que se accede
4val configuracion: Map<String, String> by lazy {
5 println("Inicializando configuración...")
6 mapOf("tema" to "oscuro", "idioma" to "es")
7}
8
9// observable — notifica cuando cambia el valor
10var puntuacion: Int by Delegates.observable(0) { _, anterior, nuevo ->
11 println("Puntuación cambió de $anterior a $nuevo")
12}
13puntuacion = 8 // Puntuación cambió de 0 a 8
14
15// vetoable — permite rechazar el cambio
16var edad: Int by Delegates.vetoable(0) { _, _, nuevaEdad ->
17 nuevaEdad >= 0 // rechaza valores negativos
18}11. Interoperabilidad con Java#
Kotlin y Java son completamente interoperables. Desde Kotlin puedes usar cualquier clase Java, y desde Java puedes usar clases Kotlin con algunas anotaciones:
1// Usar clases Java desde Kotlin — completamente transparente
2import java.util.Date
3import java.io.File
4
5val fecha = Date()
6val archivo = File("ruta/al/archivo.txt")
7
8// Anotaciones para compatibilidad con Java
9class Config {
10 @JvmField val VERSION = "1.0" // accesible como campo desde Java
11
12 companion object {
13 @JvmStatic fun crear() = Config() // método estático desde Java
14 }
15}
16
17// Manejo de null desde código Java (@Nullable / @NonNull)
18// El compilador de Kotlin trata los tipos Java como "tipos de plataforma" (T!)
19// lo que significa que pueden ser null pero no se garantiza12. Idioms — patrones idiomáticos de Kotlin#
Una colección de los patrones más usados en código Kotlin moderno:
1// ✅ Crear un singleton
2object Singleton { /* ... */ }
3
4// ✅ Patrón Result para manejo de errores (Kotlin stdlib)
5val resultado: Result<String> = runCatching {
6 operacionQuePuedeFallar()
7}
8resultado.onSuccess { dato -> println(dato) }
9resultado.onFailure { error -> println(error.message) }
10
11// ✅ Crear un DTO inmutable con valores por defecto
12data class FiltrosBusqueda(
13 val genero: String? = null,
14 val puntuacionMinima: Double = 0.0,
15 val soloFavoritos: Boolean = false
16)
17
18// ✅ Transformar un nullable en un resultado
19fun obtenerNombreCompleto(nombre: String?, apellido: String?): String =
20 listOfNotNull(nombre, apellido).joinToString(" ")
21
22// ✅ Inicialización segura de variables que dependen de otras
23val hash = nombre?.hashCode() ?: System.currentTimeMillis().toInt()
24
25// ✅ Check de instancia con cast automático
26fun procesar(obj: Any) {
27 if (obj is String) {
28 println(obj.uppercase()) // obj ya es String aquí (smart cast)
29 }
30}
31
32// ✅ Require y check para precondiciones
33fun dividir(a: Int, b: Int): Int {
34 require(b != 0) { "El divisor no puede ser 0" } // IllegalArgumentException
35 return a / b
36}