Kotlin: Coroutines y asincronía
Kotlin ofrece coroutines para manejar la asincronía de manera eficiente. Aprende cómo utilizarlas para construir aplicaciones concurrentes y mantener código limpio.
Aprende Kotlin GRATIS y certifícateLas aplicaciones modernas a menudo requieren manejar operaciones asíncronas y concurrentes, como solicitudes de red, acceso a bases de datos o procesamiento de grandes volúmenes de datos. Kotlin proporciona coroutines como una solución eficiente para manejar la asincronía sin la complejidad que implica el uso de hilos tradicionales.
Fundamentos de las coroutines
Las coroutines en Kotlin permiten escribir código asíncrono de manera secuencial, facilitando la lectura y mantenimiento del código. A diferencia de los hilos, las coroutines son ligeras y pueden ejecutarse en un mismo hilo sin bloquearlo, lo que mejora el rendimiento de las aplicaciones.
Creación y lanzamiento de coroutines
Para iniciar una coroutine, se utilizan builders proporcionados por la librería kotlinx.coroutines
, como launch
y async
.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("Coroutines en Kotlin")
}
println("Iniciando")
}
En este ejemplo:
runBlocking
es una función que inicia un bloque de coroutines y bloquea el hilo principal hasta que finaliza.launch
inicia una nueva coroutine sin bloquear el hilo actual.delay
suspende la coroutine durante el tiempo especificado sin bloquear el hilo.
Funciones de suspensión
Las funciones marcadas con el modificador suspend
pueden suspender su ejecución sin bloquear el hilo.
suspend fun fetchData(): String {
delay(1000L) // Simula una operación de red
return "Datos obtenidos"
}
Estas funciones pueden ser llamadas desde otras coroutines y permiten construir operaciones asíncronas de forma natural.
Builders de coroutines
launch
Inicia una nueva coroutine y no devuelve ningún resultado directo.
launch {
// Código asíncrono
}
async
Inicia una coroutine y devuelve un Deferred
, que representa un resultado futuro.
val resultado = async {
// Cálculos intensivos
42
}
println("El resultado es ${resultado.await()}")
Con await()
, se espera el resultado de forma asíncrona.
Contexto y alcance de las coroutines
El contexto de una coroutine incluye información como el Job
y el Dispatcher
, que determina en qué hilo o hilos se ejecuta.
Dispatchers
Dispatchers.Default
: Para operaciones intensivas en CPU.Dispatchers.IO
: Optimizado para operaciones de entrada/salida como acceso a archivos o redes.Dispatchers.Main
: Para actualizar interfaces de usuario en el hilo principal (especialmente en Android).
launch(Dispatchers.IO) {
// Operaciones de E/S
}
Manejo de excepciones
Las coroutines ofrecen mecanismos para manejar excepciones de forma estructurada.
val handler = CoroutineExceptionHandler { _, exception ->
println("Excepción capturada: ${exception.message}")
}
runBlocking {
launch(handler) {
throw RuntimeException("Error en coroutine")
}
}
CoroutineExceptionHandler
captura excepciones no manejadas en coroutines.- Es importante manejar excepciones para evitar que errores silenciosos afecten la aplicación.
Combinación de coroutines
Las coroutines pueden combinarse para ejecutar operaciones concurrentes y esperar a que todas finalicen.
suspend fun calcularResultado(): Int = coroutineScope {
val uno = async { calcularUno() }
val dos = async { calcularDos() }
uno.await() + dos.await()
}
suspend fun calcularUno(): Int {
delay(1000L)
return 20
}
suspend fun calcularDos(): Int {
delay(1000L)
return 22
}
En este ejemplo, las funciones calcularUno
y calcularDos
se ejecutan en paralelo, y se espera por ambos resultados antes de continuar.
Secuencias asíncronas con Flow
Flow
es una API fría y asíncrona para manejar flujos de datos de manera reactiva.
import kotlinx.coroutines.flow.*
fun obtenerNumeros(): Flow<Int> = flow {
for (i in 1..3) {
delay(100L)
emit(i)
}
}
fun main() = runBlocking {
obtenerNumeros().collect { valor ->
println("Recibido: $valor")
}
}
flow
construye un flujo de datos.collect
recolecta los valores emitidos.- Ideal para transmitir múltiples valores de forma asíncrona.
Cancelación de coroutines
Las coroutines son cancelables y cooperan con el mecanismo de cancelación.
val job = launch {
repeat(1000) { i ->
println("Trabajo $i")
delay(500L)
}
}
delay(2000L)
println("Cancelando coroutine")
job.cancelAndJoin()
println("Coroutine cancelada")
cancelAndJoin
cancela la coroutine y espera a que finalice.- Es importante que las coroutines sean cooperativas, utilizando funciones de suspensión que verifican el estado de cancelación.
Tiempo de espera con withTimeout
Para limitar el tiempo de ejecución de una coroutine, se utiliza withTimeout
.
try {
withTimeout(1300L) {
repeat(1000) { i ->
println("Trabajo $i")
delay(500L)
}
}
} catch (e: TimeoutCancellationException) {
println("Tiempo de espera excedido")
}
withTimeout
lanza una excepciónTimeoutCancellationException
si el bloque supera el tiempo especificado.- Permite establecer límites claros en operaciones potencialmente largas.
Estructura y alcance de coroutines
Es recomendable utilizar scopes específicos para controlar el ciclo de vida de las coroutines.
class MiClase {
private val scope = CoroutineScope(Dispatchers.Default)
fun iniciar() {
scope.launch {
// Tarea asíncrona
}
}
fun detener() {
scope.cancel() // Cancela todas las coroutines iniciadas en este scope
}
}
CoroutineScope
agrupa coroutines para una gestión conjunta.- Facilita la limpieza y cancelación cuando ya no son necesarias.
Integración en aplicaciones Android
En Android, existen scopes proporcionados por componentes de arquitectura para facilitar el uso de coroutines respetando el ciclo de vida.
viewModelScope
En ViewModels, se utiliza viewModelScope
para iniciar coroutines que se cancelan automáticamente cuando el ViewModel se destruye.
class MiViewModel : ViewModel() {
fun cargarDatos() {
viewModelScope.launch {
val datos = repositorio.obtenerDatos()
// Actualizar UI
}
}
}
lifecycleScope
En Activities y Fragments, lifecycleScope
permite lanzar coroutines que se cancelan según el estado del ciclo de vida.
class MiActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Operaciones que se cancelan en onDestroy
}
}
}
Buenas prácticas con coroutines
- Estructura jerárquica: Utilizar scopes y contextos adecuados para organizar coroutines.
- Manejo de excepciones: Capturar y manejar excepciones para evitar comportamientos inesperados.
- Evitar GlobalScope: Preferir scopes específicos para controlar el ciclo de vida y evitar fugas de memoria.
- Funciones de suspensión eficientes: Asegurarse de que las funciones marcadas como
suspend
sean realmente asíncronas o que puedan suspenderse. - Uso adecuado de Dispatchers: Seleccionar el dispatcher correcto según la naturaleza de la operación (CPU, E/S, UI).
Lecciones de este módulo de Kotlin
Lecciones de programación del módulo Coroutines y asincronía del curso de Kotlin.
Ejercicios de programación en este módulo de Kotlin
Evalúa tus conocimientos en Coroutines y asincronía con ejercicios de programación Coroutines y asincronía de tipo Test, Puzzle, Código y Proyecto con VSCode.