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 vez

2.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 usar

3.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 y

5. 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 * density

5.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")      // false

6. 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 class se diseñan para ser inmutables (todos los campos val). Si necesitas modificar un campo, usa copy() 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 enum y cuándo sealed class?

  • Usa enum cuando todas las opciones tienen la misma estructura (solo difieren en el valor de sus propiedades).
  • Usa sealed class cuando las diferentes opciones pueden tener estructuras distintas (ej. Error tiene un mensaje, Cargando no tiene nada, Exito tiene 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 resultado

8. 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 garantiza

12. 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}

Referencias y recursos#

Calendar  Última modificación: viernes, 26 de junio de 2026