Guía de Kotlin: De 0 a 100
¿Qué es Kotlin?
Kotlin es un lenguaje de programación moderno, de tipado estático y multiparadigma, creado por JetBrains (los mismos que hacen IntelliJ IDEA) y lanzado en 2011. Su primera versión estable (1.0) llegó en febrero de 2016. En 2017 Google anunció soporte oficial para Android, y en 2019 Android pasó a ser Kotlin-first.
Historia breve:
- 2011: JetBrains presenta Kotlin como proyecto open source
- 2016: Kotlin 1.0 — primera versión estable
- 2017: Google anuncia soporte oficial para Android en Google I/O
- 2018: Kotlin 1.3 — corrutinas estables, multiplataforma experimental
- 2019: Android se declara Kotlin-first
- 2020: Kotlin 1.4 — multiplataforma estable, mejoras en el compilador
- 2021: Kotlin 1.5 — sealed interfaces, nuevos tipos JVM
- 2023: Kotlin 1.9 — K2 compiler (nuevo compilador), mejoras en KMP
- 2024+: Kotlin 2.0 — K2 compiler estable, Compose Multiplatform estable
¿Dónde se usa?
- Android: El lenguaje oficial para desarrollo Android. Jetpack Compose, Android KTX, todo el ecosistema moderno está en Kotlin
- Backend / Web: Ktor (async, ligero), Spring Boot con Kotlin, http4k, Jooby
- Multiplataforma (KMP): Comparte lógica de negocio entre Android, iOS, web, desktop con Kotlin Multiplatform
- Native: Kotlin/Native compila a binarios sin JVM (iOS, macOS, Linux, Windows, WebAssembly)
- JavaScript/Web: Kotlin/JS compila a JavaScript, compatible con React, Node.js
- Data Science: Kotlin for Data Science (KotlinDL, kmath, lets-plot), compatible con Python via GraalPy
- Serverless / Cloud: AWS SDK for Kotlin, funciones Lambda en Kotlin, Ktor en GraalVM Native Image
¿Quién lo usa? Google (aplicaciones Android, Firebase), Netflix (orquestación interna), Twitter (Performance team), Pinterest (Android), Trello (Android), Uber, Square, Gradle (build tool escrita en Kotlin).
Fuente: Kotlin.org — About, Google Android Docs — Kotlin, Wikipedia — Kotlin
Prerrequisitos
Antes de empezar, necesitas:
- Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
- El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)
Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.
¿Cómo empezar? Instalación en todos los entornos
SDKMAN (Linux/macOS/WSL) — recomendado
SDKMAN es el gestor de versiones de JDK más usado. Instala, cambia y gestiona múltiples JDKs y Kotlin al instante, similar a pyenv o nvm.
curl -s https://get.sdkman.io | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk list java
sdk install java 21.0.2-temurin
sdk install kotlin
sdk default java 21.0.2-temurin
kotlin -version
IntelliJ IDEA
- Community Edition (gratis): incluye soporte Kotlin nativo. File → New → Project → Kotlin
- Plug-in Kotlin ya viene incluido desde la versión 15
- Ultimate Edition: incluye Spring Boot, Ktor, Android, y más frameworks
Android Studio
Descarga desde developer.android.com/studio. Kotlin incluido por defecto. File → New → New Project → Empty Compose Activity (templates con Kotlin).
VS Code
- Instala la extensión Kotlin (de Microsoft, language server vía Kotlin LS)
- Extensiones adicionales: Kotlin Test Runner, Gradle for Java, Kotlin Multiplatform
npm install -g kotlin-language-server(para completado y navegación)
kotlinc (compilador CLI)
# Manual (sin SDKMAN)
curl -s https://get.sdkman.io | bash && sdk install kotlin
# O descarga el zip desde GitHub releases
wget https://github.com/JetBrains/kotlin/releases/download/v2.1.0/kotlin-compiler-2.1.0.zip
unzip kotlin-compiler-*.zip
export PATH=$PATH:$(pwd)/kotlinc/bin
Gradle (build principal de Kotlin)
# Instalar SDKMAN
sdk install gradle
# O usar el wrapper (recomendado en proyectos)
gradle wrapper --gradle-version 8.5
build.gradle.kts mínimo (Kotlin DSL en Gradle):
plugins {
kotlin("jvm") version "2.1.0"
application
}
application {
mainClass = "MainKt"
}
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
}
Docker
FROM eclipse-temurin:21-jre
RUN apt-get update && apt-get install -y curl unzip \
&& curl -s https://get.sdkman.io | bash \
&& bash -c "source /root/.sdkman/bin/sdkman-init.sh && sdk install kotlin"
COPY . /app
WORKDIR /app
CMD ["kotlin", "Main.kt"]
Alias útiles para el día a día
Añade a tu ~/.bashrc o ~/.zshrc:
alias k='kotlin'
alias kotlin-repl='kotlin -version 2>/dev/null || sdk use kotlin; kotlin'
alias kg='kotlinc-jvm'
alias kw='gradle wrapper --gradle-version'
alias ktest='gradle test'
alias kbuild='gradle build'
alias krun='gradle run'
Fuente: SDKMAN docs, Kotlin Compiler manual, Docker Hub — openjdk
Escala de aprendizaje: de 0 a 100
Nivel 0–15: Fundamentos absolutos
Qué aprender:
- Sintaxis básica:
fun,val/var, tipos (Int,Double,String,Boolean) println(), templates strings"Hola $nombre"- Condicionales:
if/elsecomo expresión,when - Bucles:
foren rangos,while,forEach - Colecciones básicas:
listOf(),mapOf(),mutableListOf() - Funciones:
fun, parámetros con default, expresión body - Null safety:
String?,?.,?:,!! - Conceptos clave:
valvsvar, null safety,Intes objeto pero optimiza a primitivo
Proyecto: Adivina el número — programa que elige un número aleatorio entre 1 y 100, readln() para entrada, pistas “mayor/menor”, contador de intentos.
Nivel 15–30: Funciones, colecciones y POO básica
Qué aprender:
- Lambdas:
{ x: Int -> x * 2 },itimplícito - Funciones de orden superior:
map,filter,reduce,fold SequencevsList: lazy vs eager- Clases: constructores,
init,this - Data classes:
copy(), destructuring - Propiedades con getters/setters,
field - Extension functions,
companion object,object - Algoritmos:
sorted,sortedBy,groupingBy,fold/reduce,partition - Big O: tabla de colecciones
Proyecto: Gestor de tareas CLI — añadir, listar, completar, eliminar con data class Tarea, object GestorTareas, persistencia JSON con kotlinx.serialization.
Nivel 30–45: POO avanzada, genéricos y selladas
Qué aprender:
- Interfaces con implementación por defecto
- Sealed classes e interfaces,
whenexhaustivo - Genéricos:
<T>,out/in,reified,where - Delegados:
by lazy,by Delegates.observable, personalizados - Scope functions:
let,run,with,apply,also - Operator overloading,
inline/noinline/crossinline
Proyecto: Sistema de inventario — data class Producto, sealed class OperacionInventario, class Inventario<T>. Delegado vetoable para evitar stock negativo.
Nivel 45–60: Testing, corrutinas básicas y serialización
Qué aprender:
- Testing:
kotlin.test(JUnit 5) - Corrutinas:
runBlocking,launch,async/await,delay() suspendfunctions,Dispatchers.DefaultvsIOkotlinx.serialization:@Serializable, JSONkotlinx.coroutines.test:runTest,TestScope- Manejo de archivos:
java.io.File,Path,kotlinx.serialization
Proyecto: Gestor de contraseñas — persistencia cifrada con kotlinx.serialization, corrutinas para cifrado/descifrado, tests parametrizados.
Nivel 60–75: Ktor, Exposed y aplicaciones web
Qué aprender:
- Ktor: routing, content negotiation, serialization, plugins
- Exposed: DSL de tablas, queries, transactions
- Inyección de dependencias: Koin
- Autenticación: JWT, sesiones
- Testing de endpoints:
testApplicationde Ktor
Proyecto: API de recetas — Ktor + Exposed + H2/PostgreSQL, CRUD con autenticación JWT, tests con testApplication + BD en memoria.
Nivel 75–90: Multiplataforma (KMP), Compose y producción
Qué aprender:
- KMP: estructura de módulos
common,android,ios expect/actualdeclarations- Jetpack Compose:
@Composable,State,remember,LaunchedEffect - Compose Multiplatform: UI compartida
- CI/CD: GitHub Actions con Gradle caching
- Docker: Dockerfile multi-etapa para Ktor
Proyecto: App de notas multiplataforma — módulo shared con modelos y lógica (KMP), UI con Compose Multiplatform para Android y Desktop.
Nivel 90–100: Arquitectura, rendimiento y sistemas distribuidos
Qué aprender:
- Arquitectura limpia: capas domain, data, presentation
- Event-driven: Kafka con corrutinas, RabbitMQ
- GraphQL con
graphql-kotlin, gRPC congrpc-kotlin - Observabilidad: OpenTelemetry, Prometheus
- Rendimiento: profiling, JMH benchmarking
- GraalVM Native Image: binario nativo (
<100msstartup) - Arrow.kt: Either, Option, IO funcional
Proyecto: Sistema de notificaciones distribuidas — microservicios con Ktor y gRPC, Kafka como event bus, GraalVM Native Image, Docker Compose + Prometheus + Grafana.
Fuente: Ktor docs, Exposed docs, Kotlin Multiplatform docs
Primeros pasos y configuración del entorno
Si prefieres ver directamente el “Hola Mundo” en Kotlin, consulta El Atlas del Hola Mundo.
Editores recomendados
Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.
Kotlin REPL
Ejecuta kotlin en terminal (sin archivos) y escribe directamente:
println("Hola Mundo")
2 + 2
val lista = listOf(1, 2, 3)
lista.map { it * 2 }
Para salir: :quit o Ctrl+D.
Kotlin Playground
Navega a play.kotlinlang.org — editor online oficial de JetBrains. Soporta:
- Programas completos con
fun main() - Corrutinas, multiplataforma, Java interop
- Compartir snippets por URL
- Descargar como archivo .kt
Scripts Kotlin (.kts)
Kotlin también puede ejecutarse como script (sin compilar):
// hola.kts
fun main() {
println("Hola desde Kotlin script")
}
main()
Ejecuta: kotlin hola.kts
main() sin clase
Kotlin permite funciones de nivel superior. No necesitas clases:
// Main.kt
fun main() {
println("Hola, mundo!")
}
Compila y ejecuta:
kotlinc-jvm Main.kt -include-runtime -d main.jar
java -jar main.jar
# O usando kotlin directamente:
kotlin Main.kt
Estructura de proyecto con Gradle Kotlin DSL
mi-proyecto/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── Main.kt
│ └── test/
│ └── kotlin/
│ └── MainTest.kt
└── gradle/
└── wrapper/
Tipos y construcción básica
val entero: Int = 42
val largo: Long = 42L
val doble: Double = 3.14
val texto: String = "Hola"
val booleano: Boolean = true
val caracter: Char = 'A'
val bytes: Byte = 127
val array: IntArray = intArrayOf(1, 2, 3)
// Template strings
val nombre = "Mundo"
println("Hola $nombre!")
println("2 + 2 = ${2 + 2}")
// Rangos
for (i in 1..10) print(i)
for (i in 10 downTo 1 step 2) print(i)
for (i in 1 until 10) print(i)
// When (switch potente)
val x = 3
val resultado = when (x) {
1 -> "uno"
2 -> "dos"
in 3..10 -> "varios"
!in 0..100 -> "fuera de rango"
else -> "otro"
}
Fuente: Kotlin Playground, Kotlin docs — Basic Syntax
Paradigmas de programación
Kotlin es un lenguaje multiparadigma que combina lo mejor de la programación orientada a objetos y la funcional. No te obliga a elegir uno — puedes mezclarlos libremente.
Programación Orientada a Objetos (POO)
En Kotlin todo es un objeto: no hay tipos primitivos independientes. Incluso los números (Int, Long) son objetos con métodos y propiedades.
val numero = 42
println(numero.toString()) // "42"
println(numero.rangeTo(50)) // 42..50
println(42.compareTo(10)) // 1 (mayor que)
println((-1).coerceAtLeast(0)) // 0
Principios soportados:
- Clases y objetos:
class,object,data class,sealed class - Herencia: Jerarquía única con
open classyabstract class - Interfaces: Con propiedades abstractas y métodos con implementación por defecto
- Polimorfismo: Por subtipo, paramétrico (genéricos) y ad-hoc (extension functions)
- Encapsulación: Visibilidad
public,private,protected,internal
Programación Funcional
Kotlin incorpora elementos funcionales sin ser puramente funcional. Las funciones son ciudadanos de primera clase.
Lambdas y higher-order functions:
val numeros = listOf(1, 2, 3, 4, 5)
val pares = numeros.filter { it % 2 == 0 }
val cuadrados = numeros.map { it * it }
val suma = numeros.reduce { acc, i -> acc + i }
fun operar(a: Int, b: Int, op: (Int, Int) -> Int): Int = op(a, b)
println(operar(5, 3) { x, y -> x + y }) // 8
println(operar(5, 3) { x, y -> x * y }) // 15
Data classes (value objects funcionales):
data class Persona(val nombre: String, val edad: Int)
Pattern matching con when y sealed classes:
sealed class Tree
data class Leaf(val value: Int) : Tree()
data class Node(val left: Tree, val right: Tree) : Tree()
fun sum(tree: Tree): Int = when (tree) {
is Leaf -> tree.value
is Node -> sum(tree.left) + sum(tree.right)
}
Programación orientada a nulabilidad
Kotlin integra la nulabilidad en el sistema de tipos, haciendo que los errores de null pointer sean detectables en compilación.
var nullable: String? = null
var noNullable: String = "Hola"
val longitud = nullable?.length ?: 0
Programación reactiva
Kotlin soporta programación reactiva mediante corrutinas y flujos asíncronos. Flow, StateFlow y SharedFlow son la base del modelo reactivo en Kotlin.
val flujo = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
class ViewModel {
private val _state = MutableStateFlow("inicial")
val state: StateFlow<String> = _state.asStateFlow()
}
Tabla de paradigmas
| Paradigma | Soporte nativo | Librerías/APIs | Ejemplo |
|---|---|---|---|
| POO | Clases, interfaces, herencia, data class | kotlin stdlib | class, interface, data class |
| Funcional | Lambdas, HOF, inmutabilidad | kotlin stdlib | map, filter, reduce |
| Null Safety | T?, ?., ?:, !! | Lenguaje nativo | String?, elvis operator |
| Reactivo | Flow, StateFlow, SharedFlow | kotlinx.coroutines | flow { ... } |
| DSL | Lambdas con receiver, type-safe builders | Varios | Ktor routing, Compose UI |
Paradigma principal: Kotlin es principalmente POO + funcional. No es Haskell (puramente funcional) ni Java (puramente POO con lambdas añadidas).
Fuente: Kotlin docs — Idioms, Kotlin docs — Functional Programming, Kotlin Multiplatform docs
Tipos de datos y variables
Sistema de tipos
¿Qué es?
Kotlin tiene tipado estático y fuerte: el tipo de cada variable se conoce en tiempo de compilación y no se realizan conversiones implícitas entre tipos incompatibles. El sistema de tipos es nominal con soporte de inferencia de tipos. A diferencia de Java, Kotlin distingue en el sistema de tipos entre referencias que pueden ser nulas (T?) y las que no (T).
🔗 Wikipedia — Tipado estático, Kotlin docs — Basic Types
Sintaxis básica
val x: Int = 42
val y = "hola"
var z = 3.14
var nullable: String? = null
var noNulo: String = "Hola"
fun saludo(nombre: String): String = "Hola $nombre"
🧪 Cómo probarlo
import kotlin.test.*
class TiposTest {
@Test
fun tipadoEstatico() {
val x: Int = 42
assertEquals(42, x)
}
@Test
fun inferencia() {
val texto = "hola"
assertIs<String>(texto)
}
@Test
fun nullable() {
var nulo: String? = null
assertNull(nulo)
nulo = "hola"
assertNotNull(nulo)
}
}
💡 Memoria y rendimiento
- Los tipos no-nulables (
Int,Long) se compilan a primitivos JVM (int,long) cuando no hay nullable o genéricos - Los tipos nulables (
Int?,Long?) siempre son objetos en el heap (boxing automático) IntArrayse almacena comoint[]contiguo en heap — más eficiente queList<Int>- La inferencia de tipos se resuelve en compilación, sin impacto en runtime
| Tipo | Bits (stack) | Boxing nullable | Rango |
|---|---|---|---|
Int | 32 | Int? → objeto heap | -2³¹ a 2³¹-1 |
Long | 64 | Long? → objeto heap | -2⁶³ a 2⁶³-1 |
Float | 32 | Float? → objeto heap | ±1.4E-45 a ±3.4E+38 |
Double | 64 | Double? → objeto heap | ±4.9E-324 a ±1.8E+308 |
✅ Buenas prácticas
- ✅ Usa
valpor defecto,varsolo cuando sea estrictamente necesario - ✅ Prefiere tipos no-nulables (
Inten vez deInt?) para valores que nunca serán null - ✅ Usa
IntArrayen vez deList<Int>para colecciones grandes de primitivos - ⚠️ No confundas
==(estructural, equals) con===(referencia, identidad) - ❌ No uses
!!a menos que estés 100% seguro de que el valor no es null
🏗️ Metodología
- En proyectos nuevos, activa
explicitApi = trueen Gradle para forzar visibilidad explícita - Usa type inference para código más limpio, pero declara tipos explícitamente en APIs públicas
- En Kotlin Multiplatform, los tipos se comportan igual en todas las plataformas
🔗 Para saber más
Tipos básicos
¿Qué es?
Los tipos básicos son los bloques fundamentales de datos. En Kotlin, a diferencia de Java, todo es un objeto — incluso los números tienen métodos. Sin embargo, el compilador optimiza a primitivos JVM cuando no hay nullable de por medio.
Sintaxis básica
| Tipo | Tamaño | Ejemplo | Notas |
|---|---|---|---|
Byte | 8 bits | 127 | -128 a 127 |
Short | 16 bits | 32000 | -32768 a 32767 |
Int | 32 bits | 42 | -2³¹ a 2³¹-1 |
Long | 64 bits | 42L | Sufijo L |
Float | 32 bits | 3.14f | Sufijo f o F |
Double | 64 bits | 3.14 | Por defecto para decimales |
Boolean | ~1 bit | true / false | |
Char | 16 bits | 'A' | Unicode, comilla simple |
String | Variable | "Hola" | Inmutable, array de Chars |
val millon = 1_000_000
val hex = 0xFF_EC_DE_5E
val entero: Int = 42
val largo: Long = entero.toLong()
🧪 Cómo probarlo
import kotlin.test.*
class TiposBasicosTest {
@Test
fun conversionExplicita() {
val entero = 42
val largo = entero.toLong()
assertIs<Long>(largo)
assertEquals(42L, largo)
}
@Test
fun underscores() {
val num = 1_000_000
assertEquals(1000000, num)
}
}
💡 Memoria y rendimiento
Int,Long,Float,Doubleno-nulables son primitivos en la JVMInt?,Long?nulables son objetosInteger,Longen el heapStringes un objeto en heap con un arraychar[]interno- Las colecciones de primitivos (
List<Int>) siempre usan boxing;IntArrayno
✅ Buenas prácticas
- ✅ Usa
Int(noInteger) — Kotlin maneja la optimización a primitivo automáticamente - ✅ Prefiere
StringsobreString?cuando el valor nunca será null - ⚠️ No uses
Floatpara dinero — prefiereBigDecimaloInt(céntimos) - ❌ No asumas conversiones implícitas como en Java — en Kotlin todo es explícito
🏗️ Metodología
- Para datos numéricos pequeños usa el tipo más pequeño solo si tienes millones de instancias
- En Android,
Intes el tipo más eficiente para IDs y contadores
🔗 Para saber más
Mutabilidad: val vs var
¿Qué es?
val declara una referencia inmutable (read-only, no reasignable), mientras que var declara una referencia mutable (reasignable). val NO significa que el objeto sea inmutable — solo la referencia no se puede reasignar.
🔗 Kotlin docs — Basic Syntax: Variables
Sintaxis básica
val nombre: String = "Ana"
var edad: Int = 30
val lista = mutableListOf(1, 2, 3)
lista.add(4)
🧪 Cómo probarlo
import kotlin.test.*
class MutabilidadTest {
@Test
fun valNoReasignable() {
val x = 42
assertEquals(42, x)
}
@Test
fun varReasignable() {
var x = 42
x = 43
assertEquals(43, x)
}
@Test
fun valObjetoMutable() {
val lista = mutableListOf(1, 2, 3)
lista.add(4)
assertEquals(listOf(1, 2, 3, 4), lista)
}
}
💡 Memoria y rendimiento
valyvarno tienen diferencia en runtimeconst val(compile-time constant) se inlinea en el códigovalpermite al compilador y al lector razonar mejor sobre el código
✅ Buenas prácticas
- ✅ Regla de oro: Usa
valpor defecto. Solo usavarcuando sea estrictamente necesario - ⚠️
valno garantiza inmutabilidad profunda — el objeto referenciado puede tener estado mutable - ❌ No uses
varpara colecciones que solo se asignan una vez
🏗️ Metodología
- En programación funcional, prefiere
val+ copias (data classcopy()) sobrevar+ mutaciones - En Compose, usa
valpara el estado observable
🔗 Para saber más
Colecciones
¿Qué es?
Kotlin separa claramente colecciones mutables e inmutables (solo lectura) a nivel de tipo. Por defecto, las colecciones son inmutables.
🔗 Kotlin docs — Collections Overview
Sintaxis básica
val lista: List<Int> = listOf(1, 2, 3)
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
mutable.add(4)
val set: Set<Int> = setOf(1, 2, 3, 1, 2)
val mapa: Map<String, Int> = mapOf("uno" to 1, "dos" to 2)
val numeros = listOf(1, 2, 3, 4, 5)
val pares = numeros.filter { it % 2 == 0 }
val cuadrados = numeros.map { it * it }
🧪 Cómo probarlo
import kotlin.test.*
class ColeccionesTest {
@Test
fun inmutablePorDefecto() {
val lista = listOf(1, 2, 3)
assertEquals(3, lista.size)
}
@Test
fun mutable() {
val lista = mutableListOf(1, 2, 3)
lista.add(4)
assertEquals(listOf(1, 2, 3, 4), lista)
}
@Test
fun operacionesFuncionales() {
val numeros = listOf(1, 2, 3, 4, 5)
assertEquals(listOf(2, 4), numeros.filter { it % 2 == 0 })
}
}
💡 Memoria y rendimiento
- Acceso por índice en
List: O(1) para ArrayList - Búsqueda en
Set/Map: O(1) promedio (hash) Listpor defecto esArrayList(contiguo en memoria)Set/Mappor defecto sonLinkedHashSet/LinkedHashMap
✅ Buenas prácticas
- ✅ Declara colecciones como
List(inmutable) por defecto - ✅ Usa
listOf(),setOf(),mapOf()como constructores principales - ⚠️ No devuelvas
MutableListdesde una API pública - ❌ No uses
LinkedListcomo predeterminada
🏗️ Metodología
- En Android/Compose, usa
StateFlow<List<T>>+SnapshotStateListpara UI reactiva - En KMP, las colecciones son exactamente iguales en todas las plataformas
🔗 Para saber más
Nullabilidad
¿Qué es?
El sistema de nullabilidad de Kotlin elimina los NullPointerException en tiempo de compilación. Cualquier tipo puede ser nullable añadiendo ? al tipo. El compilador obliga a manejar el caso null antes de usar el valor.
Sintaxis básica
var nulo: String? = null
var noNulo: String = "Hola"
val longitud = nulo?.length ?: 0
val forzado = nulo!!.length
val numero = texto as? Int ?: 0
if (nulo != null) {
println(nulo.length)
}
nulo?.let { valor ->
println("El texto tiene ${valor.length} caracteres")
} ?: println("El texto es null")
🧪 Cómo probarlo
import kotlin.test.*
class NullSafetyTest {
@Test
fun safeCall() {
val nulo: String? = null
assertEquals(null, nulo?.length)
}
@Test
fun elvisOperator() {
val nulo: String? = null
assertEquals(0, nulo?.length ?: 0)
}
@Test
fun smartCast() {
val x: String? = "hola"
if (x != null) {
assertEquals(4, x.length)
}
}
}
💡 Memoria y rendimiento
String?(nullable) siempre es un objeto en heap,Stringpuede ser optimizadoInt?siempre esIntegeren heap (boxing),Intes primitivo en stack- El smart cast no tiene overhead — es solo azúcar del compilador
✅ Buenas prácticas
- ✅ Declara tipos no-nulables siempre que sea posible
- ✅ Usa
?.para acceso seguro y?:para valores por defecto - ✅ Usa
letpara operaciones encadenadas sobre valores nulables - ⚠️ Evita
!!en código de producción — es un code smell - ❌ No uses
Optionalde Java en Kotlin
🏗️ Metodología
- En APIs, devuelve
T?en vez de lanzar excepción cuando un valor puede no existir - En Android, los
BundleyIntentextras suelen devolverT?— manéjalos con Elvis
🔗 Para saber más
Arrays
¿Qué es?
Los arrays en Kotlin son contenedores de tamaño fijo. Kotlin ofrece arrays genéricos (de objetos) y arrays de primitivos (sin boxing) para rendimiento.
Sintaxis básica
val array: Array<String> = arrayOf("a", "b", "c")
val intArray = intArrayOf(1, 2, 3)
val cuadrados = IntArray(5) { i -> i * i }
🧪 Cómo probarlo
import kotlin.test.*
class ArraysTest {
@Test
fun arrayGenerico() {
val arr = arrayOf("a", "b", "c")
assertEquals("a", arr[0])
}
@Test
fun arrayPrimitivo() {
val arr = intArrayOf(1, 2, 3)
assertIs<IntArray>(arr)
}
@Test
fun inicializacionLambda() {
val cuadrados = IntArray(5) { it * it }
assertContentEquals(intArrayOf(0, 1, 4, 9, 16), cuadrados)
}
}
💡 Memoria y rendimiento
IntArray:int[]contiguo en heap, 4 bytes por elementoArray<Int>:Integer[]en heap, ~16-24 bytes por elemento (boxing)- Para colecciones grandes de primitivos,
IntArrayes 4-6x más eficiente queList<Int>
✅ Buenas prácticas
- ✅ Prefiere
IntArray/FloatArraysobreArray<Int>/Array<Float>para datos numéricos grandes - ⚠️ No uses arrays como reemplazo genérico de listas — las listas son más flexibles
- ❌ No uses arrays para APIs públicas si puedes usar
List
🔗 Para saber más
String templates
¿Qué es?
Kotlin permite incrustar variables y expresiones directamente en cadenas con el prefijo $.
🔗 Kotlin docs — String Templates
Sintaxis básica
val nombre = "Mundo"
println("Hola $nombre!")
println("2 + 2 = ${2 + 2}")
println("Suma: ${sumar(2, 3)}")
🧪 Cómo probarlo
import kotlin.test.*
class StringTemplatesTest {
@Test
fun variableSimple() {
val nombre = "Kotlin"
assertEquals("Hola Kotlin!", "Hola $nombre!")
}
@Test
fun rawString() {
val texto = """
|Línea 1
|Línea 2
""".trimMargin()
assertTrue(texto.contains("Línea 1"))
}
}
✅ Buenas prácticas
- ✅ Prefiere templates sobre concatenación con
+ - ✅ Usa raw strings (triple comilla
""") para SQL, JSON, HTML multilínea - ❌ No anides templates complejos dentro de otros templates
🔗 Para saber más
Control de flujo y modularidad
if como expresión
¿Qué es?
En Kotlin, if no es solo una sentencia — es una expresión que devuelve un valor. No hay operador ternario (cond ? a : b) porque if lo reemplaza.
Sintaxis básica
val max = if (a > b) a else b
val resultado = if (condicion) {
a + b
} else {
a - b
}
🧪 Cómo probarlo
import kotlin.test.*
class IfExpressionTest {
@Test
fun ifDevuelveValor() {
val max = if (5 > 3) 5 else 3
assertEquals(5, max)
}
}
✅ Buenas prácticas
- ✅ Usa
ifcomo expresión para asignaciones condicionales - ✅ Prefiere
whensobreif-else ifencadenados para más de 2 ramas
🔗 Para saber más
when (switch potenciado)
¿Qué es?
when es el reemplazo de Kotlin para switch de Java/C. Es mucho más potente: puede emparejar valores, rangos, tipos, condiciones booleanas, y es exhaustivo con sealed class.
Sintaxis básica
val x = 3
val texto = when (x) {
1 -> "uno"
2 -> "dos"
in 3..10 -> "varios"
!in 0..100 -> "fuera de rango"
else -> "otro"
}
when {
x < 0 -> println("Negativo")
x == 0 -> println("Cero")
x > 0 -> println("Positivo")
}
sealed class Estado
object Cargando : Estado()
data class Exito(val data: String) : Estado()
fun manejar(estado: Estado) = when (estado) {
Estado.Cargando -> "⏳"
is Estado.Exito -> "✅ ${estado.data}"
}
✅ Buenas prácticas
- ✅ Usa
whenpara 3+ ramas condicionales - ✅ Prefiere
whenexhaustivo consealed class - ⚠️ Siempre incluye
elsea menos que elwhensea exhaustivo
🔗 Para saber más
Bucles: for, while, do-while
for (i in 1..5) print(i)
for (i in 1 until 5) print(i)
for (i in 0..10 step 2) print(i)
val lista = listOf("a", "b", "c")
for (item in lista) print(item)
for ((index, value) in lista.withIndex()) {
println("$index: $value")
}
lista.forEach { println(it) }
var i = 0
while (i < 5) { println(i); i++ }
var j = 0
do { println(j); j++ } while (j < 5)
Excepciones: try/catch/finally
val resultado = try {
val dato = api.fetchData()
"Éxito: $dato"
} catch (e: IOException) {
"Error de red: ${e.message}"
} catch (e: Exception) {
"Error: ${e.message}"
} finally {
println("Operación completada")
}
fun fail(mensaje: String): Nothing {
throw IllegalStateException(mensaje)
}
val resultado2 = runCatching { api.fetchData() }
.onSuccess { println("Datos: $it") }
.onFailure { println("Error: ${it.message}") }
Módulos y organización
package com.ejemplo.util
fun String.contarVocales(): Int = count { it.lowercase() in "aeiou" }
import com.ejemplo.util.contarVocales
Visibilidad:
| Modificador | Visibilidad |
|---|---|
public (default) | Visible en cualquier parte |
private | Solo dentro del archivo o clase |
protected | Clase: visible en subclases |
internal | Visible dentro del mismo módulo Gradle |
// Módulo Gradle separado
plugins { kotlin("jvm") }
dependencies { implementation(kotlin("stdlib")) }
Fuente: Kotlin docs — Control Flow, Kotlin docs — Packages
Sistema de archivos
Lectura y escritura (kotlin.io)
¿Qué es?
Kotlin añade extensiones sobre java.io.File que simplifican la lectura y escritura de archivos. También ofrece APIs multiplataforma con kotlin.io.path para KMP.
🔗 Kotlin docs — Reading/Writing Files
Sintaxis básica
import java.io.File
import kotlin.io.path.*
val file = File("datos.txt")
val texto = file.readText()
val lineas = file.readLines()
file.forEachLine { linea -> println(linea) }
file.writeText("Nuevo contenido")
file.appendText("Más contenido")
import java.nio.file.*
val contenido = Files.readString(Path.of("/ruta/archivo.txt"))
Files.writeString(Path.of("salida.txt"), "Contenido")
// KMP Path
val path = Path("datos.txt")
path.writeText("Contenido")
val leido = path.readText()
path.copyTo(Path("copia.txt"), overwrite = true)
🧪 Cómo probarlo
import kotlin.test.*
import java.io.File
import java.nio.file.Files
class FileIOTest {
@Test
fun escribirYLeer() {
val file = File.createTempFile("test", ".txt")
file.writeText("Hola Kotlin")
assertEquals("Hola Kotlin", file.readText())
file.deleteOnExit()
}
@Test
fun forEachLine() {
val file = File.createTempFile("test", ".txt")
file.writeText("línea 1\nlínea 2\nlínea 3")
val lineas = mutableListOf<String>()
file.forEachLine { lineas.add(it) }
assertEquals(3, lineas.size)
file.deleteOnExit()
}
}
✅ Buenas prácticas
- ✅ Usa
useLines()para archivos grandes — no carga todo en memoria - ✅ Usa
Pathen KMP y en proyectos nuevos - ⚠️ Siempre especifica charset (
readText(charset)oCharsets.UTF_8) - ❌ No abuses de
readLines()en archivos grandes
🔗 Para saber más
Serialización (kotlinx.serialization)
¿Qué es?
kotlinx.serialization es la librería oficial de JetBrains para serializar/deserializar datos. Soporta múltiples formatos (JSON, ProtoBuf, CBOR, HOCON, Properties) y es multiplataforma por defecto.
Sintaxis básica
// build.gradle.kts
plugins { kotlin("plugin.serialization") version "2.1.0" }
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") }
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Usuario(
val nombre: String,
val email: String,
val edad: Int = 0,
@SerialName("es_activo") val activo: Boolean = true
)
val json = Json { prettyPrint = true; ignoreUnknownKeys = true }
val usuario = Usuario("Ana", "ana@email.com", 28)
val stringJson = json.encodeToString(usuario)
val usuarioDes = json.decodeFromString<Usuario>(stringJson)
File("usuario.json").writeText(json.encodeToString(usuario))
🧪 Cómo probarlo
import kotlin.test.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Persona(val nombre: String, val email: String)
class SerializacionTest {
@Test
fun serializarDeserializar() {
val p = Persona("Ana", "ana@email.com")
val json = Json.encodeToString(p)
val recuperado = Json.decodeFromString<Persona>(json)
assertEquals(p, recuperado)
}
}
✅ Buenas prácticas
- ✅ Usa
@SerialNamepara mapear a nombres de campo distintos en JSON - ✅ Configura
ignoreUnknownKeys = trueen APIs externas - ⚠️ No serialices clases con referencias circulares
- ❌ No uses
transientde Java — usa@Transientde kotlinx.serialization
🔗 Para saber más
WatchService (monitorización de archivos)
import java.nio.file.*
fun watchDirectory(path: String): Thread = thread {
val watchService = FileSystems.getDefault().newWatchService()
val dir = Path.of(path)
dir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
)
while (true) {
val key = watchService.take()
key.pollEvents().forEach { event ->
val filename = event.context() as Path
when (event.kind()) {
StandardWatchEventKinds.ENTRY_CREATE -> println("Creado: $filename")
StandardWatchEventKinds.ENTRY_MODIFY -> println("Modificado: $filename")
StandardWatchEventKinds.ENTRY_DELETE -> println("Eliminado: $filename")
}
}
key.reset()
}
}
Algoritmos y estructuras de datos
Ordenación
val numeros = listOf(3, 1, 4, 1, 5, 9, 2, 6)
val ordenada = numeros.sorted()
val descendente = numeros.sortedDescending()
val mutable = numeros.toMutableList()
mutable.sort()
data class Persona(val nombre: String, val edad: Int)
val personas = listOf(Persona("Ana", 28), Persona("Luis", 35))
val porNombre = personas.sortedBy { it.nombre }
val porEdadDesc = personas.sortedByDescending { it.edad }
val multi = personas.sortedWith(
compareBy<Persona> { it.edad }.thenBy { it.nombre }
)
Búsqueda
val lista = listOf(1, 3, 5, 7, 9, 11, 13)
val indice = lista.binarySearch(7)
data class Item(val id: Int, val nombre: String)
val items = listOf(Item(1, "A"), Item(3, "B"), Item(5, "C"))
val idx = items.binarySearch { it.id.compareTo(3) }
val encontrado = lista.find { it > 5 }
groupingBy, partition, chunked
val palabras = listOf("kotlin", "java", "python", "rust", "go", "c", "ruby")
val porLetra = palabras.groupBy { it.first() }
val conteo = palabras.groupingBy { it.first() }.eachCount()
val numeros = listOf(1, 2, 3, 4, 5, 6)
val (pares, impares) = numeros.partition { it % 2 == 0 }
val grupos = numeros.chunked(3)
val ventanas = numeros.windowed(3)
Tabla Big O
| Operación | ArrayList | LinkedList | HashSet | LinkedHashSet | HashMap |
|---|---|---|---|---|---|
| Acceso (get) | O(1) | O(n) | O(1)* | O(1)* | O(1)* |
| Búsqueda (contains) | O(n) | O(n) | O(1)* | O(1)* | O(1)* |
| Inserción | O(n) | O(1) | O(1)* | O(1)* | O(1)* |
| Eliminación | O(n) | O(1) | O(1)* | O(1)* | O(1)* |
| Ordenación | O(n log n) | O(n log n) | — | — | — |
* Asumiendo buena función hash
Fuente: Kotlin docs — Collections Overview
Conceptos clave explicados a fondo
Null Safety
¿Qué es?
El sistema de nullability de Kotlin distingue en el sistema de tipos entre referencias que pueden ser nulas (T?) y las que no (T). Es la característica que más bugs evita en el día a día. El compilador obliga a manejar el caso null antes de usar el valor.
Sintaxis básica
var noNulo: String = "Hola"
noNulo = null // ❌ Error de compilación
var siNulo: String? = "Hola"
siNulo = null // ✅
val texto: String? = obtenerTexto()
val longitud = texto?.length ?: 0
val longitudForzada = texto!!.length
val numero = texto as? Int ?: 0
fun procesar(obj: Any) {
when (obj) {
is String -> println(obj.length)
is Int -> println(obj + 1)
}
}
texto?.let { valor ->
println("El texto tiene ${valor.length} caracteres")
} ?: println("El texto es null")
🧪 Cómo probarlo
import kotlin.test.*
class NullSafetyTest {
@Test
fun smartCastTrasCheck() {
val x: String? = "hola"
if (x != null) {
assertEquals(4, x.length)
}
}
@Test
fun elvisConNull() {
val x: String? = null
assertEquals("default", x ?: "default")
}
@Test
fun letConNull() {
var llamado = false
val x: String? = null
x?.let { llamado = true }
assertFalse(llamado)
}
}
✅ Buenas prácticas
- ✅ Declara tipos no-nulables por defecto
- ✅ Usa
?.para acceso seguro y?:para valores por defecto - ✅ Usa
letpara operaciones encadenadas sobre valores nulables - ⚠️ Evita
!!— es un code smell - ❌ No uses
Optionalde Java en Kotlin
🔗 Para saber más
Propiedades y backing fields
¿Qué es?
Las propiedades en Kotlin encapsulan getters y setters. El compilador genera automáticamente el campo de respaldo (field) y los accesos. Puedes personalizar getters, setters, visibilidad y añadir validación.
Sintaxis básica
class Usuario {
var nombre: String = "Anónimo"
get() = field.uppercase()
set(value) {
field = value.trim()
}
var edad: Int = 0
private set
lateinit var database: Database
val config: Config by lazy {
Config.load()
}
}
🧪 Cómo probarlo
import kotlin.test.*
class PropiedadesTest {
@Test
fun getterPersonalizado() {
class User {
var nombre: String = ""
get() = field.uppercase()
set(value) { field = value.trim() }
}
val user = User()
user.nombre = " ana "
assertEquals("ANA", user.nombre)
}
@Test
fun lazy() {
var contador = 0
val valor by lazy { contador++; 42 }
assertEquals(42, valor)
assertEquals(1, contador)
}
}
✅ Buenas prácticas
- ✅ Usa
private setpara propiedades públicas de solo lectura externa - ✅ Usa
lateinitpara inyección de dependencias - ✅ Usa
by lazypara configuración costosa que no siempre se necesita - ⚠️ No accedas a
lateinitantes de inicializar
🔗 Para saber más
Data Classes
¿Qué es?
Las data class son clases diseñadas exclusivamente para contener datos. El compilador genera automáticamente equals(), hashCode(), toString(), copy() y componentN() (destructuring).
Sintaxis básica
data class Usuario(
val nombre: String,
val email: String,
val edad: Int = 0,
val activo: Boolean = true
)
val usuario = Usuario("Ana", "ana@email.com", 28)
val usuario2 = usuario.copy(edad = 30)
val (nombre, email) = usuario
🧪 Cómo probarlo
import kotlin.test.*
class DataClassTest {
@Test
fun equalsPorValor() {
val a = Usuario("Ana", "a@b.com", 28)
val b = Usuario("Ana", "a@b.com", 28)
assertEquals(a, b)
}
@Test
fun copyModifica() {
val original = Usuario("Ana", "a@b.com", 28)
val copia = original.copy(edad = 30)
assertEquals(30, copia.edad)
}
@Test
fun destructuring() {
val usuario = Usuario("Ana", "a@b.com", 28)
val (nombre, email) = usuario
assertEquals("Ana", nombre)
}
}
✅ Buenas prácticas
- ✅ Usa
data classpara modelos, DTOs, eventos - ⚠️ No heredes de data classes
- ❌ No añadas lógica de negocio en data classes
🔗 Para saber más
Sealed Classes
¿Qué es?
Las sealed class representan jerarquías de clases restringidas: todas las subclases posibles se conocen en tiempo de compilación. Son ideales para representar estados, resultados, o uniones discriminadas.
🔗 Kotlin docs — Sealed Classes
Sintaxis básica
sealed class Resultado<out T> {
data class Exito<T>(val datos: T) : Resultado<T>()
data class Error(val mensaje: String, val codigo: Int = 0) : Resultado<Nothing>()
object Cargando : Resultado<Nothing>()
}
fun <T> procesar(resultado: Resultado<T>): String = when (resultado) {
is Resultado.Exito -> "Éxito: ${resultado.datos}"
is Resultado.Error -> "Error: ${resultado.mensaje} (código ${resultado.codigo})"
Resultado.Cargando -> "Cargando..."
}
sealed interface Estado {
data class Activo(val desde: Long) : Estado
data class Inactivo(val motivo: String) : Estado
object Pendiente : Estado
}
✅ Buenas prácticas
- ✅ Usa sealed class para estados de UI (Loading, Success, Error)
- ✅ Usa sealed class para
Result<T>funcional - ⚠️ Las subclases deben estar en el mismo archivo o paquete
🔗 Para saber más
Object, Companion Object y Object Expressions
object Config {
val version = "1.0"
fun cargar() { println("Cargando configuración...") }
}
class Usuario(val nombre: String) {
companion object Factory {
fun crear(nombre: String): Usuario = Usuario(nombre.trim())
val anonimo = Usuario("Anónimo")
}
}
val listener = object : ClickListener {
override fun onClick() { println("Clickeado!") }
}
Extension Functions
¿Qué es?
Las extension functions permiten añadir métodos a clases existentes sin herencia ni decoradores. Se resuelven estáticamente (no son polimórficas).
Sintaxis básica
fun String.esPalindromo(): Boolean = this == this.reversed()
fun <T> List<T>.segundo(): T? = this.getOrNull(1)
val <T> List<T>.ultimo: T?
get() = if (isEmpty()) null else this[lastIndex]
infix fun Int.esMultiploDe(otro: Int): Boolean = this % otro == 0
✅ Buenas prácticas
- ✅ Usa extension functions para añadir comportamiento a clases que no puedes modificar
- ⚠️ No declares extension functions que oculten métodos miembros
- ❌ No abuses: si usas la misma extensión en 10+ archivos, considera una librería
🔗 Para saber más
Operator Overloading
data class Punto(val x: Int, val y: Int) {
operator fun plus(otro: Punto) = Punto(x + otro.x, y + otro.y)
operator fun minus(otro: Punto) = Punto(x - otro.x, y - otro.y)
operator fun times(escalar: Int) = Punto(x * escalar, y * escalar)
}
val p1 = Punto(3, 4)
val p2 = Punto(1, 2)
println(p1 + p2)
class Operacion {
operator fun invoke(a: Int, b: Int): Int = a + b
}
val suma = Operacion()
println(suma(3, 4))
🔗 Kotlin docs — Operator Overloading
Scope Functions
| Función | Contexto | Retorno | Caso de uso |
|---|---|---|---|
let | it | Resultado | Operaciones con null safety |
run | this | Resultado | Computar un valor |
with | this | Resultado | Configurar sin retornar |
apply | this | Objeto mismo | Configurar objetos |
also | it | Objeto mismo | Efectos secundarios |
data class Persona(var nombre: String = "", var edad: Int = 0)
val persona = Persona().apply {
nombre = "Ana"
edad = 28
}
val texto: String? = "Hola"
val longitud = texto?.let { it.length } ?: 0
val info = persona.run { "$nombre tiene $edad años" }
val lista = mutableListOf(1, 2, 3).also {
println("Creando lista con ${it.size} elementos")
}
🔗 Kotlin docs — Scope Functions
Genéricos: Reified, Variance y Proyecciones
inline fun <reified T> esTipo(obj: Any): Boolean = obj is T
interface Productor<out T> { fun obtener(): T }
interface Consumidor<in T> { fun procesar(valor: T) }
val strings: List<String> = listOf("a", "b")
val any: List<Any> = strings
fun <T> clonarIfComparable(item: T): T
where T : Comparable<T>, T : Cloneable = item
DSL Builders con Receiver
class HTML {
private val children = mutableListOf<String>()
fun body(init: BODY.() -> Unit) {
val body = BODY(); body.init(); children.add(body.toString())
}
override fun toString() = "<html>${children.joinToString("")}</html>"
}
class BODY {
private val children = mutableListOf<String>()
fun h1(text: String) { children.add("<h1>$text</h1>") }
fun p(text: String) { children.add("<p>$text</p>") }
override fun toString() = "<body>${children.joinToString("")}</body>"
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML(); html.init(); return html
}
val pagina = html {
body {
h1("Título")
p("Esto es un párrafo")
}
}
🔗 Kotlin docs — Type-Safe Builders
inline, noinline, crossinline
inline fun <T> medirTiempo(block: () -> T): T {
val inicio = System.nanoTime()
val resultado = block()
println("Tardó ${(System.nanoTime() - inicio) / 1_000_000}ms")
return resultado
}
inline fun operacion(
block1: () -> Unit,
noinline block2: () -> Unit
) {
block1()
ejecutarLater(block2)
}
inline fun operar(crossinline block: () -> Unit) {
ejecutarEnOtroContexto { block() }
}
Delegados (by)
val config: Config by lazy { Config.load() }
var nombre: String by Delegates.observable("inicial") { _, old, new ->
println("Cambió de '$old' a '$new'")
}
var edad: Int by Delegates.vetoable(0) { _, _, new -> new >= 0 }
class Usuario(map: Map<String, Any?>) {
val nombre: String by map
val edad: Int by map
}
class Trimmed {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
value = newValue.trim()
}
}
var texto: String by Trimmed()
POO y patrones de diseño
Sistema de biblioteca multimedia (ejemplo completo)
import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
data class Metadatos(
val titulo: String,
val duracionSegundos: Int,
val tags: List<String> = emptyList(),
val fechaCreacion: LocalDateTime = LocalDateTime.now()
) {
val duracionFormateada: String
get() {
val min = duracionSegundos / 60
val seg = duracionSegundos % 60
return "$min:${seg.toString().padStart(2, '0')}"
}
}
sealed class EstadoReproduccion {
data class Reproduciendo(val desde: LocalDateTime) : EstadoReproduccion()
data class Pausado(val progreso: Int) : EstadoReproduccion()
object Detenido : EstadoReproduccion()
data class Error(val mensaje: String) : EstadoReproduccion()
}
interface Contenido {
val metadatos: Metadatos
var estado: EstadoReproduccion
fun reproducir(): String
fun obtenerInfo(): String
fun duracionFormateada(): String = metadatos.duracionFormateada
}
class Audio(override val metadatos: Metadatos, val bitrateKbps: Int) : Contenido {
override var estado: EstadoReproduccion = EstadoReproduccion.Detenido
private var reproducciones: Int = 0
override fun reproducir(): String {
reproducciones++
estado = EstadoReproduccion.Reproduciendo(LocalDateTime.now())
return "▶️ Reproduciendo ${metadatos.titulo}..."
}
override fun obtenerInfo(): String =
"🎵 ${metadatos.titulo} | ${metadatos.duracionFormateada} | ${bitrateKbps}kbps"
}
class Video(override val metadatos: Metadatos, val resolucion: String, val formato: String) : Contenido {
override var estado: EstadoReproduccion = EstadoReproduccion.Detenido
var subtitulos: Boolean = false
override fun reproducir(): String {
estado = EstadoReproduccion.Reproduciendo(LocalDateTime.now())
return "▶️ Reproduciendo ${metadatos.titulo}..."
}
override fun obtenerInfo(): String =
"🎬 ${metadatos.titulo} | $resolucion | $formato | ${if (subtitulos) "CC" else "Sin subtítulos"}"
}
interface Exportable {
fun exportar(formato: String = "json"): String {
return if (formato == "json") {
"{\"tipo\": \"${this::class.simpleName}\", \"titulo\": \"${(this as? Contenido)?.metadatos?.titulo}\"}"
} else {
"Exportación a $formato no soportada"
}
}
}
object Logger {
var nivel: String = "INFO"
private val logs = mutableListOf<String>()
fun log(mensaje: String) {
val entrada = "[${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)}] [$nivel] $mensaje"
logs.add(entrada)
println(entrada)
}
fun historial(): List<String> = logs.toList()
}
class Podcast(metadatos: Metadatos, bitrateKbps: Int, val episodio: Int, val anfitrion: String)
: Contenido, Exportable {
private val audio = Audio(metadatos, bitrateKbps)
override val metadatos: Metadatos get() = audio.metadatos
override var estado: EstadoReproduccion
get() = audio.estado
set(value) { audio.estado = value }
override fun reproducir(): String = audio.reproducir()
override fun obtenerInfo(): String =
"🎙️ Ep.$episodio: ${metadatos.titulo} | con $anfitrion"
fun emitir() { Logger.log("Emitiendo podcast episodio $episodio con $anfitrion") }
}
class ListaReproduccion<T : Contenido>(val nombre: String) {
private val items = mutableListOf<T>()
fun agregar(item: T) { items.add(item) }
fun reproducirTodo(): List<String> = items.map { it.reproducir() }
fun infoCompleta(): List<String> = items.map { it.obtenerInfo() }
val size: Int get() = items.size
operator fun get(indice: Int): T = items[indice]
override fun toString(): String = "📋 $nombre ($size items)"
}
fun main() = runBlocking {
val cancion = Audio(Metadatos("Bohemian Rhapsody", 354, tags = listOf("rock")), bitrateKbps = 320)
val pelicula = Video(Metadatos("Inception", 8880), resolucion = "4K", formato = "MKV").apply {
subtitulos = true
}
listOf(cancion, pelicula).forEach { println(it.obtenerInfo()) }
val lista = ListaReproduccion("Favoritos")
lista.agregar(cancion)
lista.agregar(pelicula)
println(lista.reproducirTodo())
}
Conceptos demostrados:
| Concepto | Cómo aparece |
|---|---|
| Interface | Contenido con propiedades abstractas y método concreto |
| Data class | Metadatos con copy(), destructuring |
| Sealed class | EstadoReproduccion con pattern matching exhaustivo |
| Object (singleton) | Logger con estado global |
| Composición | Podcast delega en Audio |
| Genéricos | ListaReproduccion<T : Contenido> con bound superior |
| Operator overloading | operator fun get() |
| Scope functions | apply para configurar pelicula |
| Herencia múltiple vía interfaces | Podcast implementa Contenido y Exportable |
Patrones de diseño en Kotlin
| Patrón | Cómo se implementa en Kotlin | Ejemplo |
|---|---|---|
| Strategy | Lambdas + typealias | typealias Estrategia = (List<Int>) -> List<Int> |
| Observer | Delegates.observable / StateFlow | var x by Delegates.observable(init) { ... } |
| Singleton | object | object Config { ... } |
| Factory | companion object + extension | fun Procesador.Companion.crear(...) |
| Builder | apply + DSL | Persona().apply { nombre = "Ana" } |
| Adapter | Extension functions | fun SistemaViejo.comoNuevo() |
Strategy con funciones
typealias EstrategiaOrdenacion = (List<Int>) -> List<Int>
val ascendente: EstrategiaOrdenacion = { it.sorted() }
val descendente: EstrategiaOrdenacion = { it.sortedDescending() }
class Analizador(private val estrategia: EstrategiaOrdenacion) {
fun analizar(datos: List<Int>): List<Int> = estrategia(datos)
}
Builder con apply
data class Cafe(val tipo: String = "expresso", val tamano: String = "mediano",
val leche: String = "entera", val extras: List<String> = emptyList())
fun cafe(init: CafeBuilder.() -> Unit): Cafe = CafeBuilder().apply(init).build()
class CafeBuilder {
var tipo: String = "expresso"
var tamano: String = "mediano"
var leche: String = "entera"
private val extras = mutableListOf<String>()
fun extra(extra: String) { extras.add(extra) }
fun build(): Cafe = Cafe(tipo, tamano, leche, extras)
}
val miCafe = cafe { tipo = "latte"; tamano = "grande"; leche = "avena"; extra("canela") }
🔗 Para saber más
Polimorfismo en detalle
¿Qué es?
Kotlin tiene tres formas de polimorfismo que coexisten en el mismo código: polimorfismo por subtipo (POO clásico), polimorfismo paramétrico (genéricos), y polimorfismo ad-hoc (extension functions).
1. Polimorfismo por subtipo
interface Animal {
fun sonido(): String
}
class Perro : Animal {
override fun sonido(): String = "Guau"
}
class Gato : Animal {
override fun sonido(): String = "Miau"
}
fun hacerSonido(animal: Animal) {
println(animal.sonido())
}
hacerSonido(Perro()) // Guau
hacerSonido(Gato()) // Miau
2. Polimorfismo paramétrico (genéricos)
interface Caja<T> {
fun obtener(): T
fun guardar(item: T)
}
class CajaSimple<T>(private var item: T) : Caja<T> {
override fun obtener(): T = item
override fun guardar(item: T) { this.item = item }
}
fun <T : Comparable<T>> maximo(a: T, b: T): T = if (a > b) a else b
println(maximo(3, 7))
println(maximo("Ana", "Luis"))
3. Polimorfismo ad-hoc (extension functions)
interface Sonoro {
fun sonido(): String
}
fun Sonoro.describir(): String = "Este animal hace: ${sonido()}"
class Perro : Sonoro {
override fun sonido(): String = "Guau"
}
fun describirAnimal(animal: Sonoro) {
println(animal.describir())
}
fun Int.describir(): String = "Número: $this"
fun String.describir(): String = "Texto: $this"
println(42.describir())
println("hola".describir())
| Tipo | Java | Kotlin | Swift |
|---|---|---|---|
| Subtipo | interface + implements | interface + : | protocol + : |
| Paramétrico | <T> (type erasure) | <T> (reified en inline) | <T> |
| Ad-hoc | No directo | Extension functions | Protocol Extensions |
🔗 Para saber más
Interacción con contenido multimedia
En JVM (Java 2D, ImageIO, JavaFX)
import java.awt.*
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import java.io.File
fun crearImagen(): BufferedImage {
val img = BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB)
val g = img.createGraphics()
g.color = Color.WHITE; g.fillRect(0, 0, 400, 300)
g.color = Color.RED; g.fillOval(50, 50, 100, 100)
g.color = Color.BLUE; g.font = Font("Arial", Font.BOLD, 24)
g.drawString("Hola Kotlin", 150, 150)
g.dispose()
return img
}
ImageIO.write(crearImagen(), "png", File("salida.png"))
val imagen = ImageIO.read(File("foto.jpg"))
println("Ancho: ${imagen.width}, Alto: ${imagen.height}")
JavaFX (aplicaciones desktop):
// build.gradle.kts
plugins { id("org.openjfx.javafxplugin") version "0.1.0" }
javafx { version = "21"; modules = listOf("javafx.controls", "javafx.media") }
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.media.Media
import javafx.scene.media.MediaPlayer
import javafx.scene.layout.StackPane
import javafx.stage.Stage
import java.io.File
class ReproductorAudio : Application() {
override fun start(stage: Stage) {
val media = Media(File("audio.mp3").toURI().toString())
val player = MediaPlayer(media)
player.play()
stage.scene = Scene(StackPane(), 200.0, 100.0)
stage.show()
}
}
fun main() { Application.launch(ReproductorAudio::class.java) }
Multiplataforma
| Plataforma | API principal |
|---|---|
| JVM | Java 2D / JavaFX / Swing |
| Android | Bitmap / Canvas / OpenGL ES |
| iOS | Core Graphics / Metal / AVFoundation |
| Web (JS) | Canvas API / WebGL |
Android
Bitmap y Canvas:
import android.graphics.*
val bitmap = Bitmap.createBitmap(400, 300, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint().apply {
color = Color.RED
style = Paint.Style.FILL
textSize = 48f
}
canvas.drawCircle(200f, 150f, 100f, paint)
canvas.drawText("Hola", 160f, 160f, Paint().apply {
color = Color.WHITE; textSize = 32f
})
MediaPlayer y ExoPlayer:
import android.media.MediaPlayer
val player = MediaPlayer.create(context, R.raw.mi_audio)
player.start()
player.release()
// ExoPlayer (moderno)
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
val exoPlayer = ExoPlayer.Builder(context).build()
exoPlayer.setMediaItem(MediaItem.fromUri("https://example.com/video.mp4"))
exoPlayer.prepare()
exoPlayer.play()
Coil (carga de imágenes Kotlin-first):
// build.gradle.kts
dependencies { implementation("io.coil-kt:coil:2.6.0") }
import coil.load
imageView.load("https://example.com/imagen.jpg") {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
crossfade(true)
size(400, 300)
}
Fuente: Android Developer — Graphics, Coil docs
Bases de datos
SQL
| Librería | Async | ORM | Para qué |
|---|---|---|---|
| Exposed | Sí (corrutinas) | DSL + ORM ligero | Framework oficial de JetBrains |
| Room | Sí (corrutinas) | ORM | Estándar Android con DAOs anotados |
| SQLDelight | Sí | Genera Kotlin desde SQL | Multiplataforma |
| Spring Data JPA | Sí (WebFlux) | ORM completo | Ecosistema Spring Boot |
// Exposed (DSL)
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object Usuarios : Table() {
val id = integer("id").autoIncrement()
val nombre = varchar("nombre", 100)
val email = varchar("email", 255).uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(Usuarios)
Usuarios.insert { it[nombre] = "Ana"; it[email] = "ana@email.com" }
val usuarios = Usuarios.selectAll().toList()
println(usuarios)
}
}
// Room (Android)
@Entity
data class Usuario(@PrimaryKey val id: Int, val nombre: String, val email: String)
@Dao
interface UsuarioDao {
@Query("SELECT * FROM usuario")
suspend fun obtenerTodos(): List<Usuario>
@Insert
suspend fun insertar(usuario: Usuario)
}
NoSQL
| Librería | BD | Async |
|---|---|---|
| MongoDB Kotlin Driver | MongoDB | Sí |
| kmongo | MongoDB | Sí |
| redis-kotlin | Redis | Sí |
// MongoDB Kotlin Driver
val client = MongoClient.connect("mongodb://localhost:27017")
val db = client.getDatabase("app")
val coleccion = db.getCollection<Document>("usuarios")
suspend fun insertar(nombre: String, email: String) {
coleccion.insertOne(Document("nombre", nombre).append("email", email))
}
Fuente: Exposed docs, SQLDelight docs
WebSockets y mensajería
| Librería | Para qué |
|---|---|
| Ktor WebSocket | Servidor/cliente WebSocket nativo |
| Spring WebSocket | Integrado en Spring Boot |
| OkHttp WebSocket | Cliente para Android/JVM |
// Ktor WebSocket (servidor)
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.websocket.*
import io.ktor.http.*
import io.ktor.server.routing.*
import io.ktor.server.plugins.*
fun main() {
embeddedServer(Netty, port = 8080) {
install(WebSockets)
routing {
webSocket("/chat") {
send(Frame.Text("Bienvenido al chat!"))
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
send(Frame.Text("Echo: $text"))
}
}
}
}
}.start(wait = true)
}
// OkHttp WebSocket (cliente Android)
import okhttp3.*
val client = OkHttpClient()
val request = Request.Builder().url("ws://echo.websocket.org").build()
val ws = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send("Hola servidor!")
}
override fun onMessage(webSocket: WebSocket, text: String) {
println("Recibido: $text")
}
})
// Spring WebSocket (servidor)
@Configuration
@EnableWebSocket
class WebSocketConfig : WebSocketConfigurer {
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(MiHandler(), "/ws").setAllowedOrigins("*")
}
}
class MiHandler : TextWebSocketHandler() {
private val sessions = mutableSetOf<WebSocketSession>()
override fun afterConnectionEstablished(session: WebSocketSession) {
sessions.add(session)
session.sendMessage(TextMessage("Conectado!"))
}
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
sessions.forEach { it.sendMessage(TextMessage("Echo: ${message.payload}")) }
}
}
Fuente: Ktor WebSocket docs, OkHttp WebSocket
Concurrencia y paralelismo
launch vs async
fun main() = runBlocking {
val job: Job = launch {
delay(1000)
println("Tarea completada")
}
job.join()
val deferred: Deferred<String> = async {
delay(500)
"Resultado"
}
println(deferred.await())
val resultados = listOf(
async { fetchUserData() },
async { fetchPosts() },
async { fetchComments() }
)
resultados.awaitAll()
}
Structured Concurrency
fun main() = runBlocking {
try {
coroutineScope {
launch { delay(1000); println("Tarea 1") }
launch { delay(500); throw RuntimeException("Error en tarea 2") }
}
} catch (e: Exception) {
println("Ámbito cancelado: ${e.message}")
}
}
supervisorScope {
launch { delay(1000); println("Tarea 1") }
launch { delay(500); throw RuntimeException("Error") }
}
Dispatchers
| Dispatcher | Pool de hilos | Uso típico |
|---|---|---|
Dispatchers.Default | Núcleos CPU (mínimo 2) | Cómputo, parsing |
Dispatchers.IO | 64 hilos (por defecto) | Red, BD, archivos |
Dispatchers.Main | UI thread | Android: actualizar vistas |
Dispatchers.Unconfined | El hilo actual | Testing, prototipos |
withContext(Dispatchers.Default) { procesarDatos(datos) }
withContext(Dispatchers.IO) { api.fetchUsers() }
Flow (cold async streams)
fun flujoDatos(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.flowOn(Dispatchers.Default)
fun main() = runBlocking {
flujoDatos()
.map { it * 2 }
.filter { it > 4 }
.catch { e -> println("Error: $e") }
.collect { println(it) }
}
StateFlow vs SharedFlow (hot streams)
class ViewModel {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
private val _eventos = MutableSharedFlow<UiEvent>()
val eventos: SharedFlow<UiEvent> = _eventos.asSharedFlow()
fun cargarDatos() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val datos = api.fetchData()
_uiState.value = UiState.Success(datos)
_eventos.emit(UiEvent.DatosCargados)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Error")
}
}
}
}
Channel (comunicación productor-consumidor)
val channel = Channel<String>(capacity = Channel.BUFFERED)
fun main() = runBlocking {
launch(Dispatchers.Default) {
repeat(5) {
channel.send("Mensaje $it")
delay(100)
}
channel.close()
}
for (msg in channel) {
println("Recibido: $msg")
}
}
Tabla de usos
| Escenario | Herramienta |
|---|---|
| Llamada a API | async / withContext(IO) |
| Tareas paralelas CPU | async en Dispatchers.Default |
| Stream de datos en tiempo real | Flow |
| Estado de UI | StateFlow |
| Eventos de un solo disparo | SharedFlow |
| Cola productor/consumidor | Channel |
Manejo de excepciones
fun main() = runBlocking {
val deferred = async { throw RuntimeException("Error") }
try { deferred.await() }
catch (e: Exception) { println("Capturado: ${e.message}") }
val handler = CoroutineExceptionHandler { _, exception ->
println("Handler capturó: ${exception.message}")
}
supervisorScope {
launch { throw RuntimeException("Error 1") }
launch { delay(200); println("Tarea 2 completada") }
}
}
¿Cómo se compara?
- Threads (Java): Pesados (~1MB/hilo), difíciles de cancelar. Corrutinas: ~100 bytes/cada una
- CompletableFuture (Java): Verboso, sin structured concurrency
- async/await (JS/TS): Similar sintaxis, pero Kotlin añade Flow, Channel, StateFlow, Dispatchers
- Swift concurrency:
async/await+Task+AsyncSequence— muy similar conceptualmente
Fuente: Kotlin docs — Coroutines, Kotlin Coroutines Guide
Testing y calidad
Frameworks y herramientas
| Framework | Propósito | Async | CLI |
|---|---|---|---|
| kotlin.test | Unitario (JUnit 5) | Sí | gradle test |
| Kotest | BDD, property-based | Sí | gradle test |
| MockK | Mocking | Sí | gradle test |
| Kover | Cobertura | — | gradle koverReport |
Cómo testear cada concepto
import kotlin.test.*
// Variables y tipos — aserciones simples
@Test
fun tiposBasicos() {
val x = 42
assertIs<Int>(x)
assertEquals(42, x)
}
// Control de flujo — cobertura de ramas
@Test
fun whenExhaustivo() {
fun clasificar(x: Int) = when {
x < 0 -> "negativo"
x == 0 -> "cero"
else -> "positivo"
}
assertEquals("negativo", clasificar(-1))
assertEquals("cero", clasificar(0))
assertEquals("positivo", clasificar(1))
}
// POO — mocks con MockK
@Test
fun mockConMockK() {
val repo = mockk<Repositorio> {
every { obtenerDatos() } returns listOf("test")
}
assertEquals(1, repo.obtenerDatos().size)
}
// Corrutinas — runTest
@Test
fun corrutinas() = runTest {
val resultado = async { "test" }
assertEquals("test", resultado.await())
}
// File I/O — temp directories
@Test
fun archivos() {
val dir = createTempDir()
val file = File(dir, "test.txt")
file.writeText("contenido")
assertTrue(file.exists())
assertEquals("contenido", file.readText())
dir.deleteRecursively()
}
Parametrización
// JUnit 5 parametrizado
@ParameterizedTest
@ValueSource(ints = [1, 2, 3, 4, 5])
fun esPositivo(numero: Int) {
assertTrue(numero > 0)
}
// Kotest property-based
class PropTest : StringSpec({
"reverse debería mantener longitud" {
forAll<String> { str ->
str.reversed().length == str.length
}
}
})
Cobertura y CI
# Kover (oficial JetBrains)
gradle koverReport
# JaCoCo (alternativa)
gradle jacocoTestReport
# GitHub Actions básico
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '21', distribution: 'temurin' }
- run: gradle test --no-daemon
Fuente: Kotlin docs — Testing, Kotest docs, MockK docs
Conceptos avanzados para Kotlin
Corrutinas: Flow y Channels avanzado
Flow: Stream frío que emite valores bajo demanda. Es backpressure-aware y se integra con structured concurrency.
// Flow con combinación de fuentes
data class User(val id: Int, val name: String)
data class Order(val userId: Int, val total: Double)
fun getUsers(): Flow<User> = flow {
emit(User(1, "Ana")); emit(User(2, "Luis"))
}
fun getOrders(): Flow<Order> = flow {
emit(Order(1, 100.0)); emit(Order(1, 50.0)); emit(Order(2, 200.0))
}
// Combinar flujos
fun main() = runBlocking {
getUsers().flatMapConcat { user ->
getOrders().filter { it.userId == user.id }
.map { "${user.name}: ${it.total}" }
}.collect { println(it) }
}
// Flow con buffer y conflate
flow {
for (i in 1..10) { emit(i); delay(100) }
}.buffer(10)
.conflate()
.collect { println(it) }
Channels: Comunicación productor-consumidor con diferentes tipos de capacidad.
// Channel RENDEZVOUS (sin buffer, bloqueante)
val rendezvous = Channel<String>(Channel.RENDEZVOUS)
// Channel BUFFERED (buffer fijo)
val buffered = Channel<String>(10)
// Channel CONFLATED (solo el último valor)
val conflated = Channel<String>(Channel.CONFLATED)
// Channel UNLIMITED (sin límite, cuidado con OOM)
val unlimited = Channel<String>(Channel.UNLIMITED)
// Fan-out: múltiples consumidores
fun main() = runBlocking {
val channel = Channel<Int>(10)
repeat(3) { id ->
launch {
for (msg in channel) {
println("Worker $id: $msg")
}
}
}
repeat(10) { channel.send(it) }
channel.close()
}
Kotlin Multiplatform (KMP)
Comparte lógica de negocio entre Android, iOS, web y desktop desde un mismo código.
// build.gradle.kts (módulo KMP)
plugins {
kotlin("multiplatform") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
}
kotlin {
androidTarget {
compilations.all { kotlinOptions.jvmTarget = "17" }
}
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework { baseName = "shared" }
}
sourceSets {
commonMain.dependencies {
implementation(kotlin("stdlib-common"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}
androidMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
}
}
}
// commonMain — código compartido
expect fun platformName(): String
class Greeting {
fun greet(): String = "Hola desde ${platformName()}"
}
// androidMain — implementación Android
actual fun platformName(): String = "Android"
// iosMain — implementación iOS
actual fun platformName(): String = "iOS"
Compose Multiplatform
UI declarativa compartida entre Android, iOS, Desktop y Web.
@Composable
fun App() {
var count by remember { mutableStateOf(0) }
MaterialTheme {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Has pulsado $count veces")
Button(onClick = { count++ }) {
Text("Pulsar")
}
}
}
}
// main (Android + Desktop)
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
Conceptos clave de Compose:
@Composable: función que describe la UIState: estado observable que provoca recomposiciónremember: mantiene estado entre recomposicionesLaunchedEffect: lanza corrutinas en composablesModifier: cadena de modificadores (tamaño, padding, click)
🔗 Para saber más
Proyecto final integrador: API de recetas con Ktor
Este proyecto combina file system, algoritmos, POO, testing y red.
// ── Modelos (POO + data class) ──
data class Ingrediente(val nombre: String, val cantidad: Double, val unidad: String)
data class Receta(
val id: Int,
val titulo: String,
val ingredientes: List<Ingrediente>,
val instrucciones: String,
val tiempoMinutos: Int
)
// ── Repositorio (file system + algoritmos) ──
class RepositorioRecetas(private val archivo: java.io.File) {
private val recetas = mutableListOf<Receta>()
init {
if (archivo.exists()) {
archivo.forEachLine { linea ->
val partes = linea.split("|")
if (partes.size >= 4) {
recetas.add(Receta(
id = partes[0].toIntOrNull() ?: 0,
titulo = partes[1],
ingredientes = emptyList(),
instrucciones = partes[2],
tiempoMinutos = partes[3].toIntOrNull() ?: 0
))
}
}
}
}
fun buscarPorTiempo(maxMinutos: Int): List<Receta> =
recetas.filter { it.tiempoMinutos <= maxMinutos }
.sortedBy { it.tiempoMinutos }
fun buscarPorTexto(query: String): List<Receta> =
recetas.filter {
it.titulo.contains(query, ignoreCase = true) ||
it.instrucciones.contains(query, ignoreCase = true)
}
fun agregar(receta: Receta) {
recetas.add(receta)
guardar()
}
private fun guardar() {
archivo.writeText(recetas.joinToString("\n") { r ->
"${r.id}|${r.titulo}|${r.instrucciones}|${r.tiempoMinutos}"
})
}
}
// ── API REST con Ktor ──
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.request.*
fun main() {
val repo = RepositorioRecetas(java.io.File("recetas.txt"))
embeddedServer(Netty, port = 8080) {
routing {
get("/recetas") {
val maxTiempo = call.request.queryParameters["maxTiempo"]?.toIntOrNull()
val query = call.request.queryParameters["q"]
val resultado = when {
maxTiempo != null -> repo.buscarPorTiempo(maxTiempo)
query != null -> repo.buscarPorTexto(query)
else -> emptyList()
}
call.respond(resultado)
}
post("/recetas") {
val receta = call.receive<Receta>()
repo.agregar(receta)
call.respond(HttpStatusCode.Created, receta)
}
}
}.start(wait = true)
}
Tests
import kotlin.test.*
import java.io.File
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.testing.*
import io.ktor.http.*
class RecetasTest {
@Test
fun repositorioPersiste() {
val file = File.createTempFile("recetas", ".txt")
val repo = RepositorioRecetas(file)
repo.agregar(Receta(1, "Tortilla", emptyList(), "Cocinar", 10))
val repo2 = RepositorioRecetas(file)
assertEquals(1, repo2.buscarPorTexto("Tortilla").size)
file.deleteOnExit()
}
@Test
fun apiGetRecetas() = testApplication {
application { /* configurar módulo Ktor */ }
val response = client.get("/recetas?q=pollo")
assertEquals(HttpStatusCode.OK, response.status)
}
}
Despliegue a producción
Build para producción
./gradlew build -x test
El JAR se genera en build/libs/.
Docker multi-stage
FROM gradle:8-jdk21 AS builder
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
RUN gradle dependencies --no-daemon
COPY src ./src
RUN gradle build -x test --no-daemon
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
CMD ["java", "-jar", "app.jar"]
Hosting
- Fly.io — despliegue con
flyctl launch - Railway — conecta tu repo y despliega automáticamente
- Heroku — despliegue con Git o Container Registry
CI/CD (GitHub Actions)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- run: ./gradlew build -x test
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Canales y recursos en español
YouTube
- Kotlin en Español — Curso desde cero, corrutinas, Android, Ktor
- Midulive — Kotlin, desarrollo multiplataforma, proyectos prácticos
- Develoteca — Kotlin para Android, Jetpack Compose, MVVM
- CodigoFacilito — Kotlin desde fundamentos hasta apps Android
- Programación ATS — Curso de Kotlin básico
- MoureDev — Kotlin, Android y desarrollo de apps
- Platzi — Ruta de Kotlin con proyectos Android
Comunidades
- r/Kotlin (Reddit) — Noticias, discusiones, proyectos
- Kotlin en Español (Telegram) — Grupo activo de ayuda
- Kotlin Developers (Discord) — Comunidad internacional
- Stack Overflow en español — Etiqueta
kotlin
Repositorios
- JetBrains/kotlin — Código fuente del lenguaje
- Kotlin/kotlinx.coroutines — Corrutinas
- Kotlin/ktor — Framework web asíncrono
- JetBrains/compose-multiplatform — Compose Multiplatform
- JetBrains/Exposed — ORM/Database
- Awesome Kotlin — Lista curada de librerías
Blogs y newsletters
- Kotlin Weekly — Newsletter semanal
- JetBrains Blog — Kotlin — Anuncios oficiales
- Android Developers — Kotlin — Guías oficiales
Hacks y tips de productividad
1. Type-safe builders con apply
val alertDialog = AlertDialog.Builder(context).apply {
setTitle("Confirmar")
setMessage("¿Estás seguro?")
setPositiveButton("Sí") { _, _ -> accion() }
setNegativeButton("No", null)
}.create()
2. Sealed class + when exhaustivo para estados de UI
sealed class UiState {
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
object Loading : UiState()
}
fun render(state: UiState) = when (state) {
is UiState.Success -> mostrarLista(state.data)
is UiState.Error -> mostrarError(state.message)
UiState.Loading -> mostrarSpinner()
}
3. inline + reified para type-safe APIs
inline fun <reified T> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)
data class Usuario(val nombre: String, val email: String)
val usuario: Usuario = gson.fromJson(json)
4. runCatching para operaciones que pueden fallar
fun dividir(a: Int, b: Int): Result<Int> = runCatching { a / b }
dividir(10, 2)
.onSuccess { println("Resultado: $it") }
.onFailure { println("Error: ${it.message}") }
5. Sequence para lazy processing
val lista = (1..1_000_000).toList()
val lazy = lista.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
.toList()
6. Extension functions para logging
fun Any.logInfo(mensaje: String) {
println("[${this::class.simpleName}] $mensaje")
}
7. require() y check() para precondiciones
data class Persona(val nombre: String, val edad: Int) {
init {
require(nombre.isNotBlank()) { "El nombre no puede estar vacío" }
require(edad >= 0) { "La edad no puede ser negativa" }
}
}
8. takeIf y takeUnless para filtros inline
val numero = 42
val par = numero.takeIf { it % 2 == 0 }
val impar = numero.takeUnless { it % 2 == 0 }
9. Comandos esenciales
gradle build # Compila + tests
gradle test # Solo tests
gradle koverReport # Cobertura
10. Type aliases para tipos complejos
typealias JsonMap = Map<String, Any>
typealias Callback<T> = (Result<T>) -> Unit
typealias IdUsuario = Long
Fuente: Kotlin docs — Idioms, Kotlin docs — Coding Conventions
⚠️ Errores comunes
Si algo falla, lo más probable es que esté en esta tabla:
| Error | Causa | Solución |
|---|---|---|
Unresolved reference: X | Variable, función o clase no declarada, o falta un import | Añade el import necesario; revisa la ortografía y el scope |
Type mismatch: inferred type is X but Y was expected | Pasas un tipo a un parámetro o variable de tipo incompatible | Usa .toX() de conversión, o ajusta el tipo declarado; revisa nullability (X?) |
Smart cast to X is impossible, because 'Y' is a mutable property | Intentas smart cast en una propiedad var que puede cambiar entre comprobación y uso | Usa una variable local val, sincroniza con synchronized, o aplica as explícito |
Unresolved reference: println | Falta el import kotlin.io.* o estás en un contexto que no lo incluye automáticamente | Añade import kotlin.io.println o usa println() que está en el paquete raíz de Kotlin |
Could not resolve all files for configuration ':classpath' | Gradle no puede resolver las dependencias del plugin (repositorio mal configurado o sin red) | Verifica repositories { mavenCentral(); google() } en el bloque buildscript |
Unresolved reference to gradle | El wrapper gradlew no existe o el proyecto no fue sincronizado | Ejecuta gradle wrapper o sincroniza el proyecto en IntelliJ/Android Studio |
'gradle' is not recognized as an internal or external command | Gradle no está instalado o no está en el PATH | Instala Gradle desde gradle.org o usa el wrapper ./gradlew |
🔗 Guías relacionadas
- Java de 0 a 100 — la base JVM sobre la que corre Kotlin: Guía de Java: De 0 a 100
- C# de 0 a 100 — otro lenguaje con null safety y coroutines equivalentes: Guía de C# y .NET: De 0 a 100
- Docker — containeriza tus apps Ktor/Android: Docker: instalación y primeros pasos
Referencias y documentación oficial
- Docs oficiales: kotlinlang.org/docs
- Kotlin Playground: play.kotlinlang.org
- Try Kotlin: try.kotlinlang.org
- Kotlin by Example: play.kotlinlang.org/byExample
- Kotlin GitHub: github.com/JetBrains/kotlin
- Kotlin Forum: discuss.kotlinlang.org
- Kotlin YouTube: youtube.com/@Kotlin
Fuentes de esta guía
Esta guía se ha elaborado a partir de las siguientes fuentes consultadas durante su desarrollo:
- Kotlin.org — Documentación oficial
- Kotlin Coroutines Guide
- Ktor — Documentación oficial
- Jetpack Compose docs
- Kotlin Multiplatform docs
- Kotlin by Example
- Kotlin in Action (Dmitry Jemerov, Svetlana Isakova)
- Atomic Kotlin (Bruce Eckel, Svetlana Isakova)
- Awesome Kotlin (GitHub)
- Kotlin Weekly Newsletter
- r/Kotlin (Reddit)
- JetBrains Kotlin Blog
- Google Android — Kotlin docs
- Refactoring Guru — Kotlin Design Patterns
🏆 Retos Relacionados
Pon a prueba lo aprendido con estos desafíos:
