Kotlin es un lenguaje de JetBrains que funciona sobre la máquina virtual de Java y es totalmente compatible con su código. Google lo recomienda para crear aplicaciones Android, y también se usa en servidores y para compartir código entre plataformas. Es más conciso que Java y evita por diseño muchos errores con valores nulos.
Aplicaciones Android
Servidores en la JVM
Código compartido entre Android, iOS y web
Para empezar: Prueba los ejemplos en el Kotlin Playground oficial, en el navegador, o instala IntelliJ IDEA o Android Studio.
Nivel 1 Básico
1
Lección 1 · Básico
Tu primer programa
Un programa de Kotlin empieza en la función main. println() escribe una línea; no hace falta punto y coma al final.
El compilador genera bytecode para la JVM (o código para Android, iOS o JavaScript, según el destino).
Respuesta:fun. Las funciones de Kotlin se declaran con fun.
2
Lección 2 · Básico
val, var y plantillas de texto
val declara un valor que no se puede reasignar y var una variable que sí. Usa val siempre que puedas. El tipo se deduce, aunque puedes escribirlo: val edad: Int = 30.
Dentro de un texto, $nombre o ${expresión} insertan valores.
Ejemplo práctico
fun main() {
val nombre = "Ana"
var edad = 30
edad += 1
val precio: Double = 19.99
println("$nombre tiene $edad años")
println("Total: ${"%.2f".format(precio * 3)}")
// nombre = "Eva" // error: val cannot be reassigned
}
Ejercicio
¿Qué palabra declara un valor que no se puede reasignar?
Ver la solución
Respuesta: val. val es de solo asignación única; var permite cambiar el valor.
3
Lección 3 · Básico
if y when
En Kotlin, if es una expresión: devuelve un valor, así que no hace falta el operador ternario. when sustituye al switch y admite rangos (in 5..8) y condiciones.
Como when también devuelve un valor, el código queda corto y claro.
Ejemplo práctico
fun main() {
val nota = 7
val resultado = when (nota) {
in 9..10 -> "Sobresaliente"
in 5..8 -> "Aprobado"
else -> "Suspenso"
}
println(resultado) // Aprobado
val acceso = if (nota >= 5) "permitido" else "denegado"
println(acceso)
}
Ejercicio
¿Qué sustituye en Kotlin al switch de otros lenguajes?
Ver la solución
Respuesta: when. when elige una rama según el valor o la condición, y devuelve un resultado.
Encuentra los errores
Este programa debería mostrar «Aprobado» para un 7.5, pero no compila. Tiene 2 errores.
Los errores
Línea 5: Un when que devuelve un valor tiene que cubrir todos los casos: falta la rama else para las notas menores que 5. Corrección: else -> "Suspenso" }
Línea 9:"7.5" entre comillas es un String y la función espera un Double. Kotlin no convierte tipos automáticamente. Corrección: val nota = 7.5
Kotlin comprueba en la compilación que un when usado como expresión siempre tenga resultado; así no puede quedarse una nota sin clasificar.
Ver la solución
Línea 5: Un when que devuelve un valor tiene que cubrir todos los casos: falta la rama else para las notas menores que 5. Corrección: else -> "Suspenso" }
Línea 9:"7.5" entre comillas es un String y la función espera un Double. Kotlin no convierte tipos automáticamente. Corrección: val nota = 7.5
4
Lección 4 · Básico
Colecciones y bucles
listOf() crea una lista de solo lectura y mutableListOf() una que puedes modificar. mapOf("a" to 1) crea un mapa.
for (x in lista) recorre los elementos y for (i in 1..3) un rango.
Ejemplo práctico
fun main() {
val compra = mutableListOf("pan", "leche")
compra.add("huevos")
for (producto in compra) println("- $producto")
val stock = mapOf("teclado" to 10, "raton" to 0)
println(stock["teclado"]) // 10
println(stock.size) // 2
}
Ejercicio
¿Qué función crea una lista a la que puedes añadir elementos?
Ver la solución
Respuesta: mutableListOf(). listOf() es de solo lectura; mutableListOf() permite add, remove, etc.
Escribe el código
Escribe fun contarMayores(numeros: List<Int>, limite: Int): Int: devuelve cuántos números de la lista son mayores que limite, recorriéndola con un bucle for.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
fun contarMayores(numeros: List<Int>, limite: Int): Int {
var cuenta = 0
for (n in numeros) {
if (n > limite) {
cuenta++
}
}
return cuenta
}
Con las funciones de colección quedaría en una línea: numeros.count { it > limite }.
Ver la solución
fun contarMayores(numeros: List<Int>, limite: Int): Int {
var cuenta = 0
for (n in numeros) {
if (n > limite) {
cuenta++
}
}
return cuenta
}
Con las funciones de colección quedaría en una línea: numeros.count { it > limite }.
Nivel 2 Intermedio
5
Lección 5 · Intermedio
Funciones
Las funciones se declaran con fun, los parámetros llevan su tipo y el tipo devuelto va tras los dos puntos. Si la función es una sola expresión, se escribe con =.
Los parámetros pueden tener valor por defecto y pasarse por nombre, lo que evita muchas sobrecargas.
¿Por qué el segundo resultado sale 110.00000000000001?
Ver la solución
Respuesta: Porque Double es binario y no representa exactamente 0,1. Los Double siguen el estándar binario IEEE 754: 1,1 no tiene representación exacta. Para dinero se usa BigDecimal o se redondea al mostrar.
Escribe el código
Escribe fun saludo(nombre: String, formal: Boolean = false): String: devuelve "Buenos días, " seguido del nombre si formal es verdadero y "Hola, " seguido del nombre si no. Usa una plantilla de texto ($nombre).
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
fun saludo(nombre: String, formal: Boolean = false): String {
return if (formal) "Buenos días, $nombre" else "Hola, $nombre"
}
En Kotlin if es una expresión: devuelve el valor de la rama que se cumple, así que se puede poner directamente después de return (o de =).
Ver la solución
fun saludo(nombre: String, formal: Boolean = false): String {
return if (formal) "Buenos días, $nombre" else "Hola, $nombre"
}
En Kotlin if es una expresión: devuelve el valor de la rama que se cumple, así que se puede poner directamente después de return (o de =).
6
Lección 6 · Intermedio
Seguridad frente a null
Un String nunca puede ser null; si un valor puede faltar, se declara String?. El compilador no te deja usarlo sin comprobarlo antes.
?. llama a algo solo si no es null, ?: (el operador Elvis) da un valor por defecto y !! fuerza el uso y lanza un error si es null (mejor evitarlo).
Ejemplo práctico
fun main() {
val apodo: String? = null
println(apodo?.uppercase() ?: "Sin apodo") // Sin apodo
val nombre: String = "Ana"
println(nombre.length) // 3
// println(apodo.length) // error de compilación: apodo puede ser null, hay que usar ?. o !!
}
Ejercicio
Completa el operador que da un valor por defecto si es null.
val visible = apodo "Anónimo"
Ver la solución
Respuesta:?:. ?: (Elvis) devuelve la parte derecha cuando la izquierda es null.
Encuentra los errores
Este programa debería mostrar 0 cuando no hay nombre, pero el compilador lo rechaza. Tiene 2 errores de nulabilidad.
Los errores
Línea 2:texto puede ser null, así que Kotlin no deja usar .length directamente. Con ?. se obtiene null si no hay texto y con ?: se da un valor por defecto. Corrección: return texto?.length ?: 0
Línea 6: Un String normal no admite null; para poder guardarlo, el tipo tiene que ser String?. Corrección: val nombre: String? = null
Kotlin separa en el tipo lo que puede ser nulo (String?) de lo que no (String), y obliga a tratar el caso nulo antes de compilar.
Ver la solución
Línea 2:texto puede ser null, así que Kotlin no deja usar .length directamente. Con ?. se obtiene null si no hay texto y con ?: se da un valor por defecto. Corrección: return texto?.length ?: 0
Línea 6: Un String normal no admite null; para poder guardarlo, el tipo tiene que ser String?. Corrección: val nombre: String? = null
7
Lección 7 · Intermedio
Clases y data class
Las propiedades se declaran directamente en el constructor principal de la clase. Por defecto las clases no se pueden heredar; para permitirlo se marcan con open.
Una data class genera automáticamente toString, equals, hashCode y copy, ideal para clases que solo guardan datos.
Ejemplo práctico
class Cuenta(val titular: String) {
var saldo = 0.0
private set
fun ingresar(cantidad: Double) {
require(cantidad > 0) { "La cantidad debe ser positiva" }
saldo += cantidad
}
}
data class Producto(val nombre: String, val precio: Double)
fun main() {
val c = Cuenta("Ana")
c.ingresar(50.0)
println(c.saldo) // 50.0
val raton = Producto("Ratón", 12.5)
println(raton.copy(precio = 9.99)) // Producto(nombre=Ratón, precio=9.99)
}
Ejercicio
¿Qué genera automáticamente una data class?
Ver la solución
Respuesta: toString, equals, hashCode y copy. Kotlin escribe por ti esos métodos a partir de las propiedades del constructor.
Nivel 3 Avanzado
8
Lección 8 · Avanzado
Lambdas y funciones de colección
Una lambda es una función escrita entre llaves que se pasa como dato. Si tiene un solo parámetro, puedes referirte a él como it.
Las colecciones traen funciones como filter, map, sortedBy o sumOf, que se encadenan para transformar datos de forma legible.
Ejemplo práctico
data class Producto(val nombre: String, val precio: Double)
fun main() {
val productos = listOf(
Producto("Teclado", 25.9),
Producto("Ratón", 12.5),
Producto("Cable", 4.99),
)
val caros = productos.filter { it.precio > 10 }.map { it.nombre }.sorted()
println(caros) // [Ratón, Teclado]
println("%.2f".format(productos.sumOf { it.precio })) // 43,39 o 43.39 según el idioma del sistema
}
Ejercicio
En productos.filter { it.precio > 10 }, ¿qué es it?
Ver la solución
Respuesta: Cada producto, por turno. it es el nombre implícito del único parámetro de la lambda: aquí, cada producto.
9
Lección 9 · Avanzado
Interfaces, herencia y clases selladas
Una interfaz declara funciones que las clases implementan con : y override. Para heredar de una clase, esta tiene que estar marcada como open o abstract.
Una clase o interfaz sealed solo admite los subtipos declarados en su mismo paquete y módulo, de modo que when sabe cuáles son todos y no necesita else.
Ejemplo práctico
sealed interface Resultado
data class Exito(val datos: String) : Resultado
data class Fallo(val mensaje: String) : Resultado
fun describir(r: Resultado): String = when (r) {
is Exito -> "Recibido: ${r.datos}"
is Fallo -> "Error: ${r.mensaje}"
}
fun main() {
println(describir(Exito("3 pedidos")))
println(describir(Fallo("sin conexión")))
}
Ejercicio
¿Por qué el when del ejemplo no necesita else?
Ver la solución
Respuesta: Porque la interfaz sellada tiene un conjunto cerrado de subtipos. Con tipos sellados, el compilador conoce todas las variantes y comprueba que las cubres.
10
Lección 10 · Avanzado
Corrutinas
Las corrutinas permiten escribir código asíncrono como si fuera secuencial. Una función suspend puede pausarse (por ejemplo, mientras espera la red) sin bloquear el hilo.
Se usan con la biblioteca oficial kotlinx.coroutines: launch inicia una tarea y async una que devuelve un resultado con await(). En Android son la forma recomendada de hacer trabajo en segundo plano.
Ejemplo práctico
// Requiere la dependencia org.jetbrains.kotlinx:kotlinx-coroutines-core
import kotlinx.coroutines.*
suspend fun descargar(nombre: String): String {
delay(500) // simula esperar a la red sin bloquear
return "datos de $nombre"
}
fun main() = runBlocking {
val a = async { descargar("pedidos") }
val b = async { descargar("clientes") }
println(a.await())
println(b.await()) // las dos descargas han ido a la vez: unos 0,5 s en total
}
Ejercicio
Completa la palabra que marca una función que puede pausarse.
fun cargar(): String { … }
Ver la solución
Respuesta:suspend. Las funciones suspend solo pueden llamarse desde corrutinas u otras funciones suspend.
11
Lección 11 · Avanzado
Funciones de extensión
Una función de extensión añade un método a una clase que no es tuya, sin heredar de ella: fun String.iniciales(): String se usa luego como "Ana López".iniciales(). Dentro, this es el objeto sobre el que se llama.
Toda la biblioteca estándar de Kotlin está llena de ellas (filter, map, trim…). Úsalas para que el código se lea de izquierda a derecha, pero no abuses: si una función no está claramente relacionada con el tipo, mejor una normal.
Ejemplo práctico
fun String.iniciales(): String =
split(" ").filter { it.isNotBlank() }.joinToString("") { it.first().uppercase() }
fun Double.euros(): String = "%.2f €".format(this)
fun main() {
println("ana maría lópez".iniciales()) // AML
println(19.5.euros()) // 19,50 € (con el sistema en español)
}
Escribe el código
Escribe la función de extensión fun String.esPalindromo(): Boolean: devuelve true si el texto se lee igual del derecho y del revés, sin tener en cuenta espacios ni mayúsculas.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
fun String.esPalindromo(): Boolean {
val limpio = this.replace(" ", "").lowercase()
return limpio == limpio.reversed()
}
Dentro de la función, this es el propio texto (y se puede omitir). En Kotlin, == compara el contenido de los textos.
Ver la solución
fun String.esPalindromo(): Boolean {
val limpio = this.replace(" ", "").lowercase()
return limpio == limpio.reversed()
}
Dentro de la función, this es el propio texto (y se puede omitir). En Kotlin, == compara el contenido de los textos.
12
Lección 12 · Avanzado
Errores con runCatching y Result
Además de try/catch, Kotlin ofrece runCatching { … }, que ejecuta un bloque y devuelve un Result: un éxito con el valor o un fallo con la excepción. Así el error se trata como un dato más.
getOrDefault, getOrElse, onSuccess/onFailure y map permiten encadenar qué hacer en cada caso. Para errores de lógica previsibles, require(condición) { mensaje } lanza una excepción clara.
Ejemplo práctico
fun leerEdad(texto: String): Int {
val edad = texto.trim().toInt() // lanza NumberFormatException si no es un número
require(edad in 0..130) { "Edad fuera de rango: $edad" }
return edad
}
fun main() {
println(runCatching { leerEdad("42") }.getOrDefault(-1)) // 42
println(runCatching { leerEdad("abc") }.getOrDefault(-1)) // -1
runCatching { leerEdad("200") }
.onSuccess { println("Edad válida: $it") }
.onFailure { println("No válida: ${it.message}") } // No válida: Edad fuera de rango: 200
}
Respuesta: 0. runCatching captura la excepción y devuelve un Result fallido; getOrDefault(0) da entonces el valor por defecto.
13
Lección 13 · Avanzado
Agrupar y resumir colecciones
Las colecciones de Kotlin traen funciones para responder preguntas en una línea: groupBy reparte los elementos en grupos según una clave, sumOf suma lo que calcules de cada uno y maxByOrNull busca el mayor según un criterio.
associate y associateBy crean un Map a partir de una lista, y count { } cuenta los que cumplen una condición. Combinadas, sustituyen a muchos bucles con variables auxiliares.
Ejemplo práctico
data class Venta(val vendedor: String, val mes: Int, val importe: Double)
fun main() {
val ventas = listOf(
Venta("Ana", 1, 300.0), Venta("Luis", 1, 500.0),
Venta("Ana", 2, 450.0), Venta("Luis", 2, 200.0),
)
val porVendedor = ventas.groupBy { it.vendedor }.mapValues { (_, v) -> v.sumOf { it.importe } }
println(porVendedor) // {Ana=750.0, Luis=700.0}
println(ventas.maxByOrNull { it.importe }?.vendedor) // Luis
println(ventas.count { it.importe > 400 }) // 2
println(ventas.associate { "${it.vendedor}-${it.mes}" to it.importe }["Ana-2"]) // 450.0
}
Ejercicio
Completa para sumar el importe de todas las ventas.
val total = ventas. { it.importe }
Ver la solución
Respuesta:sumOf. sumOf { } calcula un valor por elemento (aquí, el importe) y los suma.
14
Lección 14 · Avanzado
object y companion object
object declara una clase con una única instancia que se crea sola la primera vez que se usa: útil para configuraciones o registros que deben ser únicos en todo el programa.
Un companion object dentro de una clase guarda lo que pertenece a la clase y no a cada objeto (lo que en Java sería static), como constantes o funciones que crean objetos de formas concretas.
Ejemplo práctico
object Contador {
private var visitas = 0
fun registrar(): Int = ++visitas
}
class Temperatura private constructor(val celsius: Double) {
companion object {
const val CERO_ABSOLUTO = -273.15
fun desdeFahrenheit(f: Double) = Temperatura((f - 32) * 5 / 9)
}
}
fun main() {
Contador.registrar()
println(Contador.registrar()) // 2
val t = Temperatura.desdeFahrenheit(212.0)
println(t.celsius) // 100.0
println(Temperatura.CERO_ABSOLUTO) // -273.15
}
Ejercicio
¿Cuántas instancias de Contador existen si lo usas desde diez sitios distintos del programa?
Ver la solución
Respuesta: Una sola, compartida. Un object es un singleton: Kotlin crea su única instancia la primera vez que se usa y todos comparten esa misma.
15
Lección 15 · Avanzado
Pruebas con kotlin.test
La biblioteca kotlin.test ofrece la anotación @Test y funciones como assertEquals(esperado, obtenido), assertTrue o assertFailsWith<Excepción> { … }. En un proyecto de Gradle, las pruebas van en src/test/kotlin y se ejecutan con ./gradlew test.
El orden de assertEquals importa para los mensajes: primero el valor esperado y después el obtenido. Si se invierten, el informe de un fallo dice justo lo contrario de lo que pasó.
Ejemplo práctico
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
fun dividir(a: Int, b: Int): Int {
require(b != 0) { "No se puede dividir entre 0" }
return a / b
}
class CalculadoraTest {
@Test
fun divideNumerosExactos() {
assertEquals(4, dividir(12, 3))
}
@Test
fun entreCeroFalla() {
assertFailsWith<IllegalArgumentException> { dividir(5, 0) }
}
}
Encuentra los errores
Estas pruebas tienen 2 errores: una no se ejecutaría nunca y otra prueba la función equivocada con los argumentos al revés.
Los errores
Línea 8: Sin la anotación @Test, el ejecutor de pruebas la ignora. Corrección: @Test fun sumaDosNumeros() {
Línea 14: Llama a sumar en vez de a restar y pone primero el valor obtenido: el esperado (2) va en primer lugar. Corrección: assertEquals(2, restar(5, 3))
Para cada prueba, piensa: ¿qué valor espero?, ¿qué función compruebo? Y escribe assertEquals(esperado, funcion(...)).
Ver la solución
Línea 8: Sin la anotación @Test, el ejecutor de pruebas la ignora. Corrección: @Test fun sumaDosNumeros() {
Línea 14: Llama a sumar en vez de a restar y pone primero el valor obtenido: el esperado (2) va en primer lugar. Corrección: assertEquals(2, restar(5, 3))
¿Has terminado? Sigue practicando
Repasa los conceptos en el diccionario, practica con estas herramientas o prueba otro lenguaje: las ideas que has aprendido (variables, condiciones, funciones, objetos…) te servirán en cualquiera.
* Porcentaje de personas que dijeron haber usado Kotlin en la Stack Overflow Developer Survey 2026: Technology (pregunta sobre lenguajes de programación, de scripts y de marcado; todas las respuestas). El progreso de las lecciones se guarda solo en este navegador.
¿Nos dejas medir las visitas?
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.