Volver a Kotlin Básico

Introducción

Las coroutines son la solución de Kotlin para programación asíncrona. Te permiten escribir código asíncrono que se ve y comporta como código síncrono, evitando el infierno de callbacks.

Configuración

Agrega la dependencia a tu proyecto:

dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }

Coroutine Básica

import kotlinx.coroutines.* fun main() = runBlocking { launch { delay(1000L) println("Mundo!") } println("Hola,") } // Salida: // Hola, // Mundo! (después de 1 segundo)

Constructores de Coroutines

runBlocking:

fun main() = runBlocking { println("Inicio") delay(1000L) println("Fin") }

launch:

fun main() = runBlocking { launch { delay(1000L) println("Tarea 1") } launch { delay(500L) println("Tarea 2") } println("Principal") } // Salida: Principal, Tarea 2, Tarea 1

async:

fun main() = runBlocking { val deferred1 = async { delay(1000L) 10 } val deferred2 = async { delay(500L) 20 } println("Resultados: ${deferred1.await()} y ${deferred2.await()}") }

Funciones Suspend

Funciones que pueden ser pausadas y reanudadas:

suspend fun obtenerUsuario(): Usuario { delay(1000L) // Simulando llamada de red return Usuario("Alicia", 25) } suspend fun obtenerPosts(): List<Post> { delay(500L) return listOf(Post("Post 1"), Post("Post 2")) } fun main() = runBlocking { val usuario = obtenerUsuario() val posts = obtenerPosts() println("Usuario: $usuario, Posts: $posts") }

Concurrencia Estructurada

fun main() = runBlocking { launch { launch { delay(500L) println("Hijo 1") } launch { delay(300L) println("Hijo 2") } println("Padre completa") } delay(1000L) } // El padre espera a que todos los hijos completen

Contexto de Coroutine y Dispatchers

Dispatchers.Default:

fun main() = runBlocking { launch(Dispatchers.Default) { // Trabajo intensivo de CPU val resultado = (1..1_000_000).sum() println(resultado) } }

Dispatchers.IO:

fun main() = runBlocking { launch(Dispatchers.IO) { // Operaciones I/O (red, archivo) val datos = obtenerDeBaseDeDatos() println(datos) } }

Dispatchers.Main:

// Solo Android - actualiza UI launch(Dispatchers.Main) { textView.text = "Cargando..." val datos = withContext(Dispatchers.IO) { obtenerDatos() } textView.text = datos }

Manejo de Excepciones

fun main() = runBlocking { val job = launch { try { delay(500L) throw Exception("Algo salió mal") } catch (e: Exception) { println("Capturado: ${e.message}") } } job.join() }

CoroutineExceptionHandler:

fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, excepcion -> println("Capturado: ${excepcion.message}") } val job = launch(handler) { throw Exception("Error!") } job.join() }

Control de Job

fun main() = runBlocking { val job = launch { repeat(1000) { i -> println("Job: $i") delay(500L) } } delay(2000L) println("Cancelando job...") job.cancel() job.join() println("Job cancelado") }

Verificando Cancelación:

suspend fun hacerTrabajo() = coroutineScope { val job = launch { repeat(1000) { i -> if (!isActive) { println("Job cancelado") return@launch } println("Trabajando: $i") delay(100L) } } delay(500L) job.cancelAndJoin() }

Timeouts

fun main() = runBlocking { try { withTimeout(1300L) { repeat(1000) { i -> println("Estoy durmiendo $i ...") delay(500L) } } } catch (e: TimeoutCancellationException) { println("Tiempo agotado!") } }

withTimeoutOrNull:

fun main() = runBlocking { val resultado = withTimeoutOrNull(1300L) { delay(1500L) "Completado" } println(resultado) // null }

Channels

Comunicación entre coroutines:

fun main() = runBlocking { val channel = Channel<Int>() launch { for (x in 1..5) { channel.send(x * x) } channel.close() } for (y in channel) { println(y) } }

Producir y Consumir:

fun CoroutineScope.producirCuadrados(): ReceiveChannel<Int> = produce { for (x in 1..5) { send(x * x) } } fun main() = runBlocking { val cuadrados = producirCuadrados() cuadrados.consumeEach { println(it) } }

Flow

Para datos en streaming:

fun simple(): Flow<Int> = flow { for (i in 1..3) { delay(100) emit(i) } } fun main() = runBlocking { simple().collect { valor -> println(valor) } }

Operadores de Flow:

fun numeros(): Flow<Int> = flow { emit(1) emit(2) emit(3) } fun main() = runBlocking { numeros() .map { it * it } .filter { it > 5 } .collect { println(it) } // 9 }

Ejemplos Prácticos

Llamadas Paralelas a API:

data class Usuario(val id: Int, val nombre: String) data class Perfil(val bio: String) suspend fun obtenerUsuario(id: Int): Usuario { delay(1000L) return Usuario(id, "Usuario $id") } suspend fun obtenerPerfil(userId: Int): Perfil { delay(500L) return Perfil("Bio para usuario $userId") } fun main() = runBlocking { val inicio = System.currentTimeMillis() // Secuencial (lento) val usuario = obtenerUsuario(1) val perfil = obtenerPerfil(usuario.id) // Paralelo (rápido) val usuarioDeferred = async { obtenerUsuario(1) } val perfilDeferred = async { obtenerPerfil(1) } val resultadoUsuario = usuarioDeferred.await() val resultadoPerfil = perfilDeferred.await() val fin = System.currentTimeMillis() println("Tomó ${fin - inicio}ms") }

Reintentar con Retraso:

suspend fun <T> reintentarConRetraso( veces: Int = 3, retrasoInicial: Long = 100, retrasoMaximo: Long = 1000, factor: Double = 2.0, bloque: suspend () -> T ): T { var retrasoActual = retrasoInicial repeat(veces - 1) { try { return bloque() } catch (e: Exception) { delay(retrasoActual) retrasoActual = (retrasoActual * factor).toLong().coerceAtMost(retrasoMaximo) } } return bloque() // Último intento } // Uso suspend fun obtenerDatos(): String { // Llamada API simulada if (Random.nextBoolean()) throw IOException("Error de red") return "Éxito" } fun main() = runBlocking { val resultado = reintentarConRetraso { obtenerDatos() } println(resultado) }

Seguimiento de Progreso:

fun descargarArchivo(): Flow<Int> = flow { for (progreso in 0..100 step 10) { delay(100) emit(progreso) } } fun main() = runBlocking { descargarArchivo().collect { progreso -> println("Progreso de descarga: $progreso%") } }

Buenas Prácticas

  • Usa funciones suspend en lugar de callbacks
  • Usa concurrencia estructurada (coroutineScope, supervisorScope)
  • Cancela coroutines cuando ya no se necesiten
  • Usa dispatchers apropiados (Default para CPU, IO para red/disco)
  • Maneja excepciones correctamente con try-catch o CoroutineExceptionHandler
  • Usa Flow para datos en streaming
  • Evita GlobalScope (usa concurrencia estructurada)

Errores Comunes

Bloquear en Coroutines:

// Malo - bloquea el hilo launch { Thread.sleep(1000L) } // Bueno - suspende la coroutine launch { delay(1000L) }

No Usar Scope Apropiado:

// Malo - scope vive para siempre GlobalScope.launch { // ... } // Bueno - atado al ciclo de vida class MiClase : CoroutineScope { override val coroutineContext = Dispatchers.Main + Job() fun hacerTrabajo() { launch { // ... } } fun limpiar() { coroutineContext.cancel() } }

Conclusión

Las coroutines hacen la programación asíncrona en Kotlin simple y eficiente. Proporcionan una forma de escribir código async que es fácil de leer, mantener y razonar.

En el próximo capítulo, exploraremos la interoperabilidad con Java.