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
suspenden 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.