Etiqueta: Kotlin Coroutines

  • Unidad 0 — Tema 4: Programación Reactiva: Domina StateFlow y SharedFlow

    Unidad 0 — Tema 4: Programación Reactiva: Domina StateFlow y SharedFlow

    En el artículo anterior de la serie (Unidad 0 — Tema 3), desmitificamos la asincronía en Kotlin a través de las corrutinas, los despachadores y los ámbitos de ejecución estructurada. Sin embargo, procesar una única llamada de red de forma asíncrona es solo el principio.

    Si echamos la vista atrás, la forma en que manejamos la asincronía y la reactividad ha cambiado drásticamente. Atrás quedaron los tiempos de RxJava con sus complejas cadenas de operadores, e incluso LiveData ha pasado a formar parte del baúl de los recuerdos de Android.

    Hoy, en 2026, la API de Kotlin Flows es el estándar indiscutible. Cuando trabajamos en Kotlin Multiplatform (KMP), los flujos reactivos no son solo una opción para el módulo de Android; son la herramienta nativa con la que comunicamos la lógica de negocio compartida con las interfaces de usuario de Jetpack Compose y SwiftUI.

    Para dominar la arquitectura multiplataforma, necesitas entender a los dos reyes del flujo «caliente»: StateFlow y SharedFlow.

    1. El concepto clave: Flujos Fríos vs. Flujos Calientes

    Antes de entrar en detalle, una distinción rápida pero vital:

    • Flujos Fríos (Flow estándar): Solo emiten datos cuando alguien los está escuchando (collecting). Si hay tres observadores, la ejecución se repite tres veces desde el principio para cada uno. Son ideales para consultas a bases de datos o peticiones de red únicas.
    • Flujos Calientes (StateFlow / SharedFlow): Están activos y emitiendo datos independientemente de si hay pantallas escuchando o no. Son como estaciones de radio: la música sigue sonando aunque apagues el auto. Múltiples observadores pueden sintonizar el mismo flujo al mismo tiempo.

    2. La Batalla Definitiva: StateFlow vs. SharedFlow

    Aunque ambos son flujos calientes, tienen propósitos arquitectónicos completamente opuestos. Confundirlos suele provocar bugs visuales extraños.

    CaracterísticaStateFlowSharedFlow
    PropósitoRepresentar un Estado (¿Cómo se ve la pantalla?).Representar un Evento (¿Qué acaba de pasar?).
    Valor InicialObligatorio. Siempre debe empezar con un estado.No lo requiere.
    Memoria (Replay)Almacena y repite siempre el último valor emitido.Por defecto no almacena nada (configurable).
    FiltradoDescarta duplicados consecutivos (distinctUntilChanged).Emite absolutamente todo, incluso si es idéntico.

    3. Lógica Compartida en KMP: Un caso práctico

    Imaginemos un componente de lógica compartida (un ViewModel o Presenter común) que gestiona el estado de un perfil y dispara una alerta de un solo uso (como un mensaje flotante o Toast).

    class PerfilViewModel {
        // 1. STATEFLOW: Guarda el estado de la pantalla
        private val _uiState = MutableStateFlow<PerfilState>(PerfilState.Loading)
        val uiState: StateFlow<PerfilState> = _uiState.asStateFlow()
    
        // 2. SHAREDFLOW: Dispara eventos únicos que no deben repetirse si la pantalla gira
        private val _eventos = MutableSharedFlow<PerfilEvent>()
        val eventos: SharedFlow<PerfilEvent> = _eventos.asSharedFlow()
    
        suspend fun actualizarNombre(nuevoNombre: String) {
            if (nuevoNombre.isBlank()) {
                // El error es un evento. Se muestra una vez y listo.
                _eventos.emit(PerfilEvent.MostrarError("El nombre no puede estar vacío"))
                return
            }
            
            _uiState.value = PerfilState.Success(nombre = nuevoNombre)
        }
    }

    💡 Por qué esto es crucial: Si usaras un StateFlow para el mensaje de error, cuando el usuario rote el teléfono, la pantalla volvería a leer el último estado guardado y mostraría el mensaje de error otra vez, frustrando al usuario. Con SharedFlow, el evento se consume y desaparece.

    4. El consumo multiplataforma: Conectando con las Vistas

    Escribir el flujo en Kotlin es solo la mitad del trabajo. ¿Cómo se consumen en las plataformas de destino?

    En Android (Jetpack Compose)

    Compose se integra de manera orgánica con los flujos de Kotlin. Usando la extensión adecuada, la recolección se detiene automáticamente cuando la app pasa a segundo plano, ahorrando batería:

    val state by viewModel.uiState.collectAsStateWithLifecycle()

    En iOS (SwiftUI)

    En el desarrollo multiplataforma moderno, consumir estos flujos desde Swift ya no requiere acoplamientos complejos (wrappers) manuales.

    Herramientas de interoperabilidad como SKIE convierten automáticamente tus StateFlow de Kotlin en propiedades nativas que exponen objetos de tipo AsyncSequence o AsyncStream en Swift. Para un desarrollador de iOS, interactuar con tu lógica reactiva se siente idéntico a usar Async/Await nativo de Apple:

    Task {
        for await state in viewModel.uiState {
            self.render(state)
        }
    }

    5. Enfoque Sénior: Prevención de fugas de recursos en iOS al recolectar flujos

    Recolectar flujos reactivos de Kotlin en Swift a través de AsyncSequence es una gozada, pero introduce un riesgo severo de fugas de memoria y consumo de batería residual si la suscripción no se cancela al salir de la pantalla:

    • El ciclo de vida en SwiftUI: En Android, collectAsStateWithLifecycle() detiene la recolección automáticamente cuando la vista pasa a STOPPED. En SwiftUI, debes asociar la recolección asíncrona al modificador .task { } de SwiftUI. Este modificador cancela la Task de Swift automáticamente cuando la vista se destruye o se desmonta del árbol visual.
    • Suscripciones calientes persistentes: Si inicias una recolección manual de un StateFlow o SharedFlow de fondo y olvidas desvincularla, el recolector de basura de Kotlin (GC) no podrá liberar la instancia de la clase compartida debido a una referencia fuerte sostenida por la clausura de Swift.
    • Buenas prácticas: Declara siempre tus flujos compartidos en componentes vinculados al ciclo de vida de los controladores de vista y utiliza en Swift llamadas estructuradas ligadas al ciclo de vida visual de SwiftUI para evitar fugas silenciosas.

    Para profundizar

    La programación reactiva con flujos tiene reglas estrictas de concurrencia. Te sugiero revisar la documentación oficial para profundizar en operadores avanzados y testing:

    Conclusión

    Diseñar tu arquitectura multiplataforma basándote en la combinación de StateFlow (para estados persistentes) y SharedFlow (para eventos efímeros) garantiza una separación de conceptos limpia y predecible. Le estás dando a las vistas de Android y iOS un canal de comunicación asíncrono, robusto y optimizado para los ciclos de vida de cada sistema operativo.

    Con este tema cerramos con broche de oro la Unidad 0 de introducción avanzada a Kotlin. Tienes todas las bases del lenguaje dominadas: nulidad, tipos sellados, asincronía y reactividad. Ahora estamos listos para adentrarnos en el desarrollo multiplataforma real y crear nuestro primer monorepo. En el próximo artículo, Unidad 1 — Tema 1: Por qué Kotlin Multiplatform no es otro Flutter/React Native y por qué deberías usarlo, abriremos el debate arquitectónico sobre por qué KMP lidera la nueva era del desarrollo móvil híbrido.

  • Unidad 0 — Tema 3: Hilos y Asincronía: Tu primera guía de Corrutinas en Kotlin

    Unidad 0 — Tema 3: Hilos y Asincronía: Tu primera guía de Corrutinas en Kotlin

    En el artículo anterior de la serie (Unidad 0 — Tema 2), descubrimos el inmenso poder de las Sealed Interfaces para diseñar interfaces libres de estados imposibles o contradictorios. Pero, ¿de dónde provienen los datos que modelan esos estados? Generalmente provienen de llamadas asíncronas a APIs o bases de datos locales.

    Hacer que una aplicación se sienta fluida no es tarea fácil. Si ejecutas una operación pesada (como descargar un archivo de internet o procesar una base de datos) en el hilo principal, la pantalla se congelará y tu usuario desinstalará la app en tres segundos.

    Históricamente, la solución era crear hilos (Threads). Pero los hilos son pesados, consumen mucha memoria y gestionarlos es un dolor de cabeza. Aquí es donde entran las Corrutinas de Kotlin: la herramienta definitiva para manejar la asincronía de forma eficiente, elegante y, sobre todo, legible.

    En este post, derribaremos la complejidad de las corrutinas y entenderemos por qué son la columna vertebral del código asíncrono en Kotlin Multiplatform (KMP).

    1. Hilos vs. Corrutinas: ¿Por qué son tan eficientes?

    A menudo se describe a las corrutinas como «hilos ligeros», pero técnicamente no lo son.

    Un hilo tradicional está ligado directamente al sistema operativo. Crear uno requiere reservar memoria dedicada (generalmente 1 MB por hilo) y cambiar de un hilo a otro (context switch) es una operación costosa para la CPU. Si intentas lanzar 100,000 hilos a la vez, cualquier dispositivo colapsará de inmediato.

    Una corrutina, en cambio, es puramente una pieza de software gestionada por el entorno de ejecución de Kotlin. Es una tarea que se ejecuta sobre un hilo. Kotlin puede gestionar cientos de miles de corrutinas simultáneamente utilizando un número muy reducido de hilos reales. Cuando una corrutina se pausa, libera el hilo físico para que otra corrutina pueda usarlo.

    2. Los tres pilares fundamentales

    Para dominar las corrutinas, solo necesitas entender tres conceptos clave:

    A. Funciones de Suspensión (suspend)

    Es la palabra clave que cambia las reglas del juego. Una función marcada con suspend tiene la capacidad única de pausar su ejecución sin bloquear el hilo en el que corre.

    suspend fun descargarDatosDelServidor(): String {
        delay(2000) // Simula una espera de red de 2 segundos sin congelar la app
        return "Datos descargados"
    }

    Mientras delay espera, el hilo físico queda libre para hacer otras cosas (como renderizar la pantalla). Cuando el tiempo pasa, la función se «reanuda» exactamente donde se quedó.

    B. Los Despachadores (Dispatchers)

    Los Dispatchers le dicen a la corrutina en qué hilo o grupo de hilos debe ejecutarse. Kotlin nos proporciona tres opciones principales por defecto:

    • Dispatchers.Main: Vinculado al hilo principal de la interfaz de usuario. Se usa para actualizar la pantalla, animaciones o interacciones rápidas. ¡Nunca metas lógica pesada aquí!
    • Dispatchers.IO: Optimizado para operaciones de entrada/salida (I/O). Es el lugar ideal para peticiones de red, leer/escribir archivos o consultar bases de datos locales.
    • Dispatchers.Default: Diseñado para tareas intensivas de CPU. Si tienes que ordenar una lista gigantesca, procesar una imagen o realizar cálculos matemáticos complejos, este es tu sitio.

    C. El Alcance (CoroutineScope)

    Las corrutinas no pueden flotar libres en el espacio; necesitan un ciclo de vida que las controle. El CoroutineScope es el contenedor que realiza el seguimiento de todas las corrutinas que lanzas dentro de él.

    Si destruyes el contenedor (por ejemplo, si el usuario cierra una pantalla y destruyes su controlador), el Scope cancelará automáticamente todas las corrutinas activas en su interior, evitando por completo las fugas de memoria (memory leaks).

    3. Uniendo las piezas: Tu primera corrutina

    ¿Cómo se ve todo esto junto? Supongamos que estamos en un componente intermedio de nuestro código compartido en KMP:

    class RepositorioUsuario(private val scope: CoroutineScope) {
        fun cargarPerfil() {
            // Lanzamos una corrutina en el scope indicando el hilo inicial
            scope.launch(Dispatchers.IO) {
                // 1. Esto se ejecuta en un hilo de fondo (IO) sin congelar la app
                val datos = descargarDatosDelServidor() 
                
                withContext(Dispatchers.Main) {
                    // 2. Cambiamos de contexto al hilo principal para tocar la UI con seguridad
                    mostrarEnPantalla(datos)
                }
            }
        }
    }

    4. El superpoder en Kotlin Multiplatform (KMP)

    Cuando llevas esto a KMP, ocurre algo espectacular. Gracias a la librería oficial kotlinx.coroutines, el código asíncrono que escribes en tu módulo compartido funciona perfectamente tanto en Android como en iOS:

    • En Android, se integra de forma nativa con los hilos de la JVM y los ciclos de vida de Jetpack (como viewModelScope).
    • En iOS, el compilador de Kotlin/Native traduce los Dispatchers para que utilicen las colas de ejecución de Apple (Grand Central Dispatch o GCD), mapeando automáticamente Dispatchers.Main con el Main Queue de iOS de forma transparente.

    5. Enfoque Sénior: Bajo el capó de la concurrencia en Kotlin/Native y iOS

    Cuando utilizas corrutinas en un entorno multiplataforma, la forma en que los hilos y la concurrencia se comportan bajo el capó difiere enormemente entre sistemas operativos debido a sus motores de ejecución nativos:

    • JVM (Android/Backend): Kotlin Coroutines corre sobre hilos Java estándar gestionados por un pool de hilos dinámico y optimizado (ExecutorService). El cambio de contexto entre Dispatchers es nativo de la máquina virtual.
    • Kotlin/Native (iOS): iOS no tiene JVM. Las corrutinas mapean sus despachadores nativos a las colas de GCD (Grand Central Dispatch) de Apple. Dispatchers.Main se asocia al Main Queue, garantizando que cualquier actualización estética de la UI ocurra de forma segura, mientras que Dispatchers.Default y Dispatchers.IO se mapean de forma coordinada a colas globales en background de iOS.
    • El nuevo modelo de memoria: En versiones antiguas de Kotlin/Native, compartir variables mutables entre hilos requería congelar (freeze) objetos, provocando caídas en tiempo de ejecución. Desde la estabilización del nuevo modelo de memoria sin restricciones de concurrencia y recolector de basura automatizado compartido, podemos compartir estado de forma segura e idéntica a la JVM utilizando primitivas de concurrencia nativas.

    Para profundizar

    Las corrutinas tienen mucha tela que cortar. Si quieres dominar el control de flujos asíncronos y la concurrencia, revisa estos recursos oficiales:

    Conclusión

    Las corrutinas transforman el código asíncrono complejo y propenso a errores (lleno de callbacks anidados) en un código secuencial que se lee de arriba a abajo como si fuera síncrono. Al entender cómo interactúan las suspend functions, los Dispatchers y los Scopes, tienes en tus manos la clave para crear aplicaciones multiplataforma ultra eficientes.

    Pero lanzar una corrutina y procesar un resultado asíncrono único es solo el inicio. ¿Qué ocurre cuando necesitamos manejar un flujo de datos continuo y en tiempo real que reacciona a los cambios del sistema? En el próximo artículo, Unidad 0 — Tema 4: Programación Reactiva: Domina StateFlow y SharedFlow, profundizaremos en el paradigma reactivo utilizando flujos calientes de alto rendimiento.