Guía de estudio 08 · Práctica 8

Actualizaciones en tiempo real, notificaciones y trabajo en segundo plano en Android

SSE, callbackFlow, ciclo de vida, notificaciones y WorkManager

30 min de lectura Tema de la Práctica 8 Independiente del ejemplo de la práctica

Introducción

Una aplicación móvil necesita conocer cambios que ocurren fuera de ella. Un pedido puede cambiar de estado, puede publicarse un mensaje o puede registrarse una nueva actividad en el servidor. La aplicación dispone de varias estrategias para conocer esos cambios, pero la elección depende de la urgencia, el consumo de recursos, la conectividad y el ciclo de vida de Android.

Esta guía explica dos situaciones que conviene distinguir: recibir información mientras una pantalla está visible y revisar información de manera diferida cuando la aplicación no está en primer plano. Para la primera se estudia Server-Sent Events (SSE), integrado con OkHttp, callbackFlow y el ciclo de vida. Para la segunda se estudian las notificaciones del sistema y la programación de trabajo persistente mediante WorkManager. También se analizan las restricciones que impone Android para proteger batería y recursos.

Al terminar, el estudiante deberá interpretar código asíncrono, justificar la elección de una estrategia de actualización, gestionar correctamente conexiones y notificaciones, y diagnosticar comportamientos que dependen del sistema operativo.

Conocimientos previos

Se recomienda dominar corrutinas (launch, suspend, cancelación), Flow, ViewModel, peticiones HTTP, Retrofit/OkHttp, permisos Android y gestión básica de sesiones. El material es independiente de una aplicación particular.


Lo imprescindible para el examen

Comparar polling, SSE y trabajo periódico; interpretar callbackFlow y awaitClose; vincular una conexión al ciclo de vida; explicar notificaciones, restricciones y deduplicación.

Se evalúa la capacidad de explicar, interpretar fragmentos y justificar decisiones. Las versiones puntuales de librerías, URLs, nombres de proyecto y comandos accesorios quedan como contexto de consulta.

Criterios transversales para interpretar esta guía

Principio transferible. Conviene distinguir la responsabilidad de una tecnología de la forma concreta en que se utilizó en la práctica. Un ejemplo de ViewModel, Retrofit, Hilt o WebSocket ilustra una solución posible; su configuración exacta no es una regla universal.

Decisión del proyecto. Los nombres, rutas, versiones, intervalos, tamaños y elecciones de librerías del caso original sirven para interpretar el código. Salvo indicación expresa, no se exige memorizarlos para el examen.

Alcance del código. Los fragmentos son recursos didácticos; pueden omitir imports, dependencias, permisos, detalles de manejo de errores o infraestructura. Comprender un fragmento no equivale a comprobar que compila como proyecto independiente.

Referencias cruzadas. La síntesis transversal de la colección reúne las distinciones entre estado y evento, persistencia, reintentos, idempotencia y límites de las pruebas.

1. Cómo se reciben cambios del servidor

1.1. Consulta periódica, eventos y notificaciones

Pull significa que el cliente pregunta si hay novedades. Push significa que un servidor puede entregar información cuando ocurre un evento, a través de un canal o infraestructura de entrega previamente establecidos.

Estrategia Cómo funciona Aplicación apropiada Limitaciones
Consulta bajo demanda El usuario solicita GET Botón «Actualizar» Puede mostrar datos antiguos entre consultas
Polling periódico en primer plano El cliente consulta cada cierto intervalo Información que cambia regularmente Aumenta peticiones y consumo
SSE El servidor entrega eventos por una respuesta HTTP abierta Novedades unidireccionales mientras se observa una pantalla Necesita conexión activa
WebSocket Conexión con comunicación bidireccional Chat, interacción simultánea Mayor complejidad y conexión sostenida
WorkManager periódico Android programa una tarea que consulta al servidor Sincronización diferible No ofrece ejecución en el instante solicitado
Servicio de push como FCM Un servicio de mensajería entrega avisos al dispositivo Notificaciones cercanas al tiempo real en segundo plano Depende del proveedor, conectividad y políticas de entrega

Concurrencia no es periodicidad, y periodicidad no es tiempo real. Ejecutar una corrutina o un Worker no convierte una consulta en un canal push.

1.2. ¿Qué ocurre cuando la aplicación sale del primer plano?

Android administra los procesos y puede detenerlos o terminarlos. Una corrutina vinculada a una Activity o a un ViewModel no está diseñada para ejecutarse indefinidamente después de que desaparece su propietario. Además, el modo Doze y las restricciones de actividad en segundo plano pueden posponer conexiones y trabajos.

No debe suponerse que deslizar una aplicación fuera de Recientes siempre mata inmediatamente su proceso: el comportamiento depende del dispositivo, el fabricante y el estado del sistema. Forzar detención desde Ajustes es una acción diferente, que impide la ejecución programada de la aplicación hasta que vuelva a ser iniciada, de acuerdo con las reglas del sistema.

1.3. Decidir por requisitos

Antes de elegir una tecnología, identifica la latencia admisible, la dirección del intercambio, si el usuario está viendo la pantalla, la frecuencia de actualización, los costes de red y las condiciones de conectividad. Un marcador que debe cambiar mientras el usuario lo observa puede utilizar SSE. Una verificación diferible de cambios puede utilizar WorkManager. Un aviso verdaderamente urgente con la app cerrada requiere valorar un mecanismo de push adecuado y sus restricciones.


2. Server-Sent Events (SSE)

2.1. Estructura de una conexión SSE

SSE se basa en una petición HTTP a la que el servidor responde con el tipo de contenido text/event-stream. La respuesta puede permanecer abierta y entregar múltiples eventos a lo largo del tiempo.

id: 42
event: actualizacion
data: {"id":42,"titulo":"Nuevo contenido"}

: heartbeat

id: 43
event: actualizacion
data: {"id":43,"titulo":"Cambio de estado"}

Una línea en blanco termina cada evento. data: contiene el contenido, event: identifica un tipo y id: permite asociar el evento con un identificador. Una línea que empieza con : representa un comentario y puede utilizarse como latido para conservar la conexión frente a tiempos de espera de algunas infraestructuras.

SSE es unidireccional: servidor hacia cliente. Si la aplicación debe enviar cambios, utiliza solicitudes HTTP separadas o considera WebSocket cuando requiera comunicación bidireccional continua.

2.2. Reconexión y Last-Event-ID

Cuando la conexión se interrumpe, el cliente puede volver a conectarse y enviar el último identificador recibido en Last-Event-ID. Si el servidor implementa recuperación de eventos y conserva el historial necesario, puede enviar lo ocurrido desde ese identificador.

El encabezado no garantiza por sí solo la entrega sin pérdidas. Esa propiedad requiere soporte del servidor, identificadores ordenables, política de retención y comportamiento claro ante eventos repetidos. Al reconectar, la interfaz también debe evitar duplicados.

2.3. Manejo de fallos

Una conexión puede terminar por cambio de red, expiración de credenciales, cierre del servidor, interrupciones de proxy o restricciones de tiempo. El cliente debe distinguir cierre, fallo temporal, fallo de autenticación y cancelación voluntaria. Reconectar indefinidamente ante un error permanente puede desperdiciar recursos.


3. Integrar SSE con Kotlin Flow

3.1. El problema de los callbacks

Muchas bibliotecas de red entregan eventos mediante callbacks. Por ejemplo, un EventSourceListener de OkHttp SSE informa cuándo llega un evento y cuándo se cierra la conexión. En cambio, el repositorio y el ViewModel pueden trabajar con una interfaz basada en Flow.

callbackFlow transforma una fuente basada en callbacks en un flujo frío: el bloque se activa cuando alguien comienza a recolectarlo.

fun observarEventos(cliente: OkHttpClient, url: String): Flow<String> = callbackFlow {
    val solicitud = Request.Builder().url(url).build()

    val listener = object : EventSourceListener() {
        override fun onEvent(
            eventSource: EventSource,
            id: String?,
            type: String?,
            data: String
        ) {
            trySend(data)
        }

        override fun onClosed(eventSource: EventSource) {
            close()
        }

        override fun onFailure(
            eventSource: EventSource,
            t: Throwable?,
            response: Response?
        ) {
            close(t)
        }
    }

    val fuente = EventSources.createFactory(cliente)
        .newEventSource(solicitud, listener)

    awaitClose { fuente.cancel() }
}

Ejemplo didáctico: requiere las dependencias/importaciones de OkHttp SSE y corrutinas. El tratamiento de errores HTTP, parseo y reconexión debe incorporarse según el contrato de la API.

3.2. Responsabilidades de las funciones

  • trySend(...) intenta enviar un evento al canal del flujo y no suspende. Su resultado puede señalar que el envío no fue aceptado.
  • close(...) indica que la fuente terminó, opcionalmente con una causa.
  • awaitClose { ... } mantiene el productor registrado hasta que termina o se cancela la recolección, y ejecuta la limpieza.

Si el listener sobrevive a la pantalla y no se cancela, es posible dejar conexiones activas innecesariamente. Por eso la liberación explícita de recursos es parte del contrato del flujo.

3.3. Reconexión controlada

Para una fuente remota recuperable, puede construirse un Flow que abre conexiones sucesivas:

fun observarContinuamente(ultimoInicial: Int): Flow<Evento> = flow {
    var ultimoId = ultimoInicial

    while (currentCoroutineContext().isActive) {
        emitirDesdeServidor(ultimoId).collect { evento ->
            ultimoId = maxOf(ultimoId, evento.id)
            emit(evento)
        }
        delay(5_000)
    }
}

emitirDesdeServidor representa una función que produce un Flow<Evento> y permite recuperar datos desde un identificador. En una aplicación real, incorpora un tratamiento de fallos transitorios y permanentes, límites de reintentos, espera progresiva cuando corresponda y un mecanismo de actualización del token si la API lo requiere.

Una cancelación debe detener también la espera y la conexión. No se debe capturar CancellationException para continuar el ciclo.

3.4. Tiempo de espera de lectura

Si un servidor envía un heartbeat cada 15 segundos y el cliente finaliza las lecturas tras 10 segundos sin datos, la conexión podría cerrarse repetidamente. La configuración del cliente debe contemplar el ritmo de los eventos y los latidos.

val clienteStream = clienteBase.newBuilder()
    .readTimeout(30, TimeUnit.SECONDS)
    .build()

Este número es ilustrativo: el valor adecuado depende de la infraestructura y del comportamiento esperado. Una conexión prolongada tampoco significa que deba dejarse abierta cuando no hay consumidores.


4. Relacionar la conexión con el ciclo de vida

4.1. Composición y visibilidad

Un Composable puede permanecer en la composición aunque la aplicación pase a segundo plano. Por ello, estar compuesto y estar visible no son condiciones equivalentes.

Mecanismo Cuándo inicia Cuándo termina o limpia Uso típico
LaunchedEffect Al entrar en composición o cambiar claves Al salir de composición o cambiar claves Efectos ligados a la composición
LifecycleStartEffect Cuando el propietario llega a STARTED En ON_STOP o al desecharse Registrar y liberar recursos mientras está visible
repeatOnLifecycle(STARTED) Al llegar a STARTED Cancela recolección al dejar ese estado Recoger flujos con ciclo de vida
viewModelScope Cuando el ViewModel lanza un trabajo Al destruirse el ViewModel Trabajo del estado de pantalla

4.2. Ejemplo con LifecycleStartEffect

@Composable
fun ActualizacionesRoute(viewModel: ActualizacionesViewModel) {
    LifecycleStartEffect(viewModel) {
        viewModel.comenzarEscucha()
        onStopOrDispose { viewModel.detenerEscucha() }
    }

    // La interfaz observa el estado expuesto por el ViewModel.
}

comenzarEscucha() debe evitar trabajos duplicados y detenerEscucha() debe cancelar el Job y liberar la conexión. Aquí la composición relaciona explícitamente la visibilidad con una operación que consume recursos.

No es la única implementación válida. También puede exponerse un flujo y recolectarlo mediante repeatOnLifecycle o combinaciones con stateIn(SharingStarted.WhileSubscribed(...)). La elección depende de dónde vive la suscripción, si se comparte y qué recursos se deben liberar.

4.3. El error de las conexiones duplicadas

Si cada recomposición ejecutara una nueva conexión, aparecerían eventos repetidos y conexiones adicionales. El cuerpo de un Composable no debe utilizarse para iniciar directamente operaciones de larga duración. Los efectos y el alcance de las corrutinas proporcionan un punto explícito de inicio y finalización.


5. Notificaciones del sistema

5.1. Una notificación no equivale a recibir datos

Una notificación es la presentación de información al usuario mediante el sistema operativo. SSE, WorkManager o FCM pueden ser mecanismos para enterarse de algo; la notificación es una posible consecuencia. También es válido actualizar una lista sin mostrar ninguna notificación.

5.2. Canal

Desde Android 8 (API 26), las notificaciones convencionales pertenecen a canales. Un canal identifica la categoría de avisos y permite que el usuario controle su importancia y comportamiento.

fun crearCanal(context: Context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val canal = NotificationChannel(
            "novedades",
            "Novedades",
            NotificationManager.IMPORTANCE_DEFAULT
        ).apply {
            description = "Cambios importantes de contenido"
        }
        context.getSystemService(NotificationManager::class.java)
            .createNotificationChannel(canal)
    }
}

Crear un canal existente con el mismo identificador es seguro, pero el usuario puede modificar después sus preferencias. La aplicación no debe depender de que un aviso se presente siempre como emergente.

5.3. Permiso desde Android 13

En Android 13 (API 33) y posteriores, la mayoría de las notificaciones requieren POST_NOTIFICATIONS, solicitado también en tiempo de ejecución cuando corresponde. Declararlo en el manifiesto no basta. La interfaz debe explicar para qué se solicita y seguir funcionando aunque se rechace.

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Precisión importante: no existe una regla universal de «dos negativas» para todas las versiones y configuraciones. El comportamiento depende del SDK objetivo, la versión del sistema y las decisiones previas del usuario. Es necesario consultar el estado del permiso y, cuando corresponda, explicar cómo habilitar notificaciones desde Ajustes.

5.4. PendingIntent

PendingIntent permite que el sistema ejecute posteriormente una operación definida por la aplicación, por ejemplo abrir una pantalla cuando el usuario toca una notificación.

val abrirApp = PendingIntent.getActivity(
    context,
    0,
    Intent(context, MainActivity::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

val notificacion = NotificationCompat.Builder(context, "novedades")
    .setSmallIcon(R.drawable.ic_notification)
    .setContentTitle("Nuevo contenido")
    .setContentText("Se registró una actualización")
    .setContentIntent(abrirApp)
    .setAutoCancel(true)
    .build()

El recurso ic_notification debe existir. El código es un fragmento ilustrativo; antes de publicar se debe verificar canal, permiso y datos que se exponen en la pantalla bloqueada. FLAG_IMMUTABLE es apropiado para este tipo de intento cuando el sistema o terceros no necesitan modificar su contenido.

5.5. Evitar avisos duplicados

Un identificador estable usado en notify(id, notificacion) permite actualizar o reemplazar la notificación asociada a ese identificador. Esto no equivale a garantizar entrega exactamente una vez: la aplicación también debe decidir qué eventos ya procesó y cuáles deben notificarse.

La política debe definir qué se considera nuevo: último evento conocido, último evento mostrado en pantalla o último evento notificado son conceptos distintos. Marcar un elemento como «visto» al cargar una lista puede ser suficiente para una práctica, pero en una aplicación real no prueba necesariamente que el usuario lo haya leído.


6. Trabajo diferido con WorkManager

6.1. Para qué sirve

WorkManager es apropiado para trabajos diferibles y persistentes que deben ejecutarse respetando las políticas de Android. No es un temporizador exacto ni un servicio de push. Los trabajos pueden persistir entre terminaciones ordinarias del proceso y reinicios del dispositivo, pero siguen sujetos a límites del sistema.

Para trabajo periódico el intervalo mínimo admitido es de 15 minutos. Esto indica una frecuencia solicitada, no que una tarea vaya a ejecutarse exactamente a los 15, 30 y 45 minutos.

6.2. Un CoroutineWorker

class RevisarNovedadesWorker(
    context: Context,
    parametros: WorkerParameters
) : CoroutineWorker(context, parametros) {

    override suspend fun doWork(): Result {
        val repositorio = (applicationContext as MiApplication)
            .container.repositorio

        return try {
            repositorio.consultarNovedades()
            Result.success()
        } catch (e: IOException) {
            Result.retry()
        } catch (e: HttpException) {
            when (e.code()) {
                429, 500, 502, 503, 504 -> Result.retry()
                else -> Result.failure()
            }
        }
    }
}

El ejemplo presupone que MiApplication declara un contenedor con el repositorio y que consultarNovedades() realiza una operación suspendida. Una implementación real distingue errores temporales, credenciales inválidas, límites de peticiones, respuestas no recuperables y excepciones inesperadas sin ocultar la cancelación.

6.3. Los tres resultados

Resultado Significado Caso posible
Result.success() Trabajo completado o no había nada por hacer No hay novedades
Result.retry() Es útil volver a intentar más adelante Red interrumpida o error 503
Result.failure() El trabajo no debe reintentarse automáticamente Solicitud permanentemente inválida

No todos los HttpException implican fracaso permanente. Un 503 puede ser transitorio, mientras que un 403 suele requerir corregir permisos o la operación solicitada. Un 401 puede requerir refrescar credenciales; la respuesta adecuada depende del contrato de autenticación de la API.

6.4. Programación periódica y unicidad

val restricciones = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()

val peticion = PeriodicWorkRequestBuilder<RevisarNovedadesWorker>(
    15, TimeUnit.MINUTES
)
    .setConstraints(restricciones)
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "revisar_novedades",
    ExistingPeriodicWorkPolicy.KEEP,
    peticion
)

KEEP conserva el trabajo periódico existente con ese nombre si ya se programó uno. Esto evita registrar trabajos equivalentes al repetir la inicialización de la aplicación. La restricción CONNECTED indica que debe existir conectividad antes de intentar el trabajo, aunque no asegura que el servidor específico esté disponible.

6.5. Restricciones y reintentos

Las restricciones evitan iniciar trabajo cuando faltan condiciones conocidas, como conectividad. Los reintentos atienden fallos que pueden surgir incluso si esas condiciones se cumplen, como un servidor temporalmente indisponible.

Los reintentos se programan con políticas de espera; no deben implementarse mediante un ciclo de consultas sin control. El sistema puede aplazar una ejecución por ahorro energético o limitaciones de cuota.

6.6. De dónde obtiene sus dependencias

Un Worker puede construirse cuando no existe ninguna pantalla. Por eso no debe depender de un NavController ni de un ViewModel de UI. Puede obtener dependencias mediante una WorkerFactory, un mecanismo de inyección de dependencias o, en un proyecto sencillo, el contenedor de la clase Application.


7. Sincronización, estado visto y consistencia

7.1. Identificar novedades

Para evitar consultar todo el historial, una API puede admitir un parámetro desde o un cursor. El cliente conserva el cursor en almacenamiento persistente y consulta eventos posteriores.

val ultimoId = store.ultimoId.first()
val nuevos = repositorio.obtenerDesde(ultimoId)

Si no existe cursor, la primera ejecución puede establecer un punto de partida sin generar una notificación por cada registro histórico. Esto debe definirse como una política del producto.

7.2. El orden de operaciones importa

Supón que llegan tres novedades. Si el cursor se guarda antes de mostrar las notificaciones y luego el proceso se interrumpe, la aplicación puede omitir avisos. Si se guarda después, podría volver a procesar algunas novedades. Este es un problema clásico de consistencia entre dos sistemas independientes: almacenamiento local y servicio de notificaciones.

Una estrategia robusta utiliza identificadores estables, operaciones idempotentes y registros de procesamiento adecuados. Dependiendo del requisito, puede priorizar evitar omisiones o evitar avisos repetidos.

7.3. Evitar notificaciones que no aportan

Si el usuario está consultando una lista que ya recibe actualizaciones por SSE, normalmente es preferible actualizar la interfaz sin emitir una notificación por cada elemento. Los avisos del sistema deben reservarse para eventos que el usuario necesita conocer fuera de esa interacción, según las preferencias y la naturaleza del producto.


8. Limitaciones de segundo plano y alternativas

8.1. Doze y política del sistema

Cuando Android entra en modos de ahorro energético, puede limitar la red y diferir trabajo programado. Por lo tanto, no es correcto prometer al usuario que una consulta de WorkManager se ejecutará a una hora exacta. Es preferible expresar su comportamiento como sincronización periódica de mejor esfuerzo.

8.2. Fuerza de detención y proceso terminado

La terminación ordinaria de un proceso es compatible con el modelo de trabajo persistente. La acción explícita de Forzar detención es distinta y puede impedir que los trabajos programados se ejecuten hasta que el usuario vuelva a iniciar la aplicación. Asimismo, retirar una tarea de Recientes no tiene un resultado idéntico en todos los dispositivos.

8.3. ¿Cuándo elegir otro mecanismo?

  • Una actualización visual continua mientras se consulta una pantalla puede resolverse mediante SSE o WebSocket.
  • Una sincronización que puede esperar se beneficia de WorkManager.
  • Una entrega cercana al tiempo real con la app en segundo plano suele requerir infraestructura de push como FCM, si el entorno la admite.
  • Un servicio en primer plano solo es apropiado para tareas visibles y justificadas que cumplan los requisitos vigentes de Android. No debe utilizarse para eludir restricciones del sistema.

La selección depende de las necesidades del producto y de las políticas de plataforma, no únicamente de la facilidad de escribir el código.


Conexión transversal: alcance y decisiones

Eventos recibidos, cursores y duplicados

callbackFlow adapta callbacks de red a un Flow, pero el protocolo decide si hay historial, confirmaciones y recuperación. Un Last-Event-ID solo permite recuperar eventos cuando el servidor conserva y retransmite lo necesario. Un identificador único puede evitar duplicados visuales en una lista, y notify(id, ...) reemplaza una notificación con el mismo ID; ninguno garantiza procesamiento remoto exactamente una vez.

Principio: abrir y cerrar recursos de red según su uso real. Decisión de la práctica: LifecycleStartEffect para una pantalla visible y WorkManager para comprobaciones periódicas; los trabajos no se ejecutan en instantes garantizados. Véanse las guías 03, 04 y 12.

9. Diagnóstico de errores frecuentes

Síntoma Causa probable Qué revisar
El stream se desconecta a intervalos regulares readTimeout inferior al intervalo entre datos o heartbeats Cliente HTTP y servidor
Los eventos aparecen duplicados Reconexiones sin cursor correcto o múltiples colectores Last-Event-ID, deduplicación y ciclo de vida
La conexión sigue abierta en segundo plano La recolección no sigue la visibilidad LifecycleStartEffect, cancelación y awaitClose
La pantalla deja de actualizarse No se observa o emite el estado correctamente Flow, StateFlow y composición
No aparece una notificación Canal inexistente, permiso denegado o notificaciones deshabilitadas Canal, permiso y Ajustes
Tocar una notificación no abre la pantalla esperada PendingIntent o navegación incompleta Intent y destino
Se notifican registros antiguos Cursor inicial y política de novedades incorrectos Estado persistido y primera ejecución
El worker no se ejecuta a los 15 minutos exactos WorkManager es inexacto Restricciones y Doze
Hay varios workers equivalentes Programación sin unicidad enqueueUniquePeriodicWork
Se realizan reintentos inútiles Todos los errores se tratan como temporales Códigos HTTP y política de recuperación
No se ejecuta tras «Forzar detención» Restricción deliberada del sistema Diferenciar forzar detención de terminación del proceso

10. Vocabulario esencial

Término Significado
Pull El cliente inicia una consulta para obtener información
Push Entrega de información iniciada por el servidor sobre un canal disponible
SSE Eventos enviados del servidor al cliente en una respuesta HTTP prolongada
WebSocket Comunicación bidireccional sobre una conexión persistente
Last-Event-ID Identificador que el cliente puede presentar al reconectar
callbackFlow Adaptación de eventos de callbacks a un flujo de Kotlin
awaitClose Limpieza y desregistro de una fuente de eventos cuando termina su flujo
Heartbeat Mensaje periódico que mantiene o verifica la conexión
Ciclo de vida Estados que describen la actividad de un componente Android
Canal Categoría de notificaciones controlable por el usuario
PendingIntent Operación que el sistema puede ejecutar posteriormente en nombre de la app
WorkManager API para trabajo diferible y persistente administrado por Android
CoroutineWorker Worker que permite ejecutar trabajo suspendido
Restricción Condición que debe cumplirse para iniciar un trabajo
Doze Mecanismo de ahorro energético que limita actividad en segundo plano
Idempotencia Propiedad que permite procesar repetidamente una operación sin multiplicar su efecto

11. Preguntas de autoevaluación

A. Comprensión conceptual

  1. ¿Qué diferencia hay entre obtener cambios mediante pull y recibirlos mediante push? Proporciona un ejemplo de cada uno.
  2. ¿Por qué una corrutina iniciada desde una pantalla no garantiza la recepción de novedades con la aplicación cerrada?
  3. Describe la estructura básica de un mensaje SSE y explica el papel de id, event, data y la línea en blanco.
  4. ¿Qué información aporta Last-Event-ID? ¿Qué requisito adicional debe satisfacer el servidor para recuperar eventos perdidos?
  5. ¿Qué función cumplen callbackFlow, trySend y awaitClose en una conexión prolongada?
  6. Explica por qué LaunchedEffect y LifecycleStartEffect no son intercambiables en todos los casos.
  7. ¿Qué elementos intervienen en la publicación y apertura de una notificación Android?
  8. ¿Por qué una tarea periódica de WorkManager no equivale a una notificación instantánea?

B. Interpretación de código

  1. En un callbackFlow se crea un EventSource, pero no se registra su cancelación en awaitClose. ¿Qué riesgo aparece al abandonar la pantalla?
  2. Un servidor envía un heartbeat cada 20 segundos y el cliente tiene readTimeout(10, TimeUnit.SECONDS). ¿Qué comportamiento cabe esperar?
  3. Un Composable ejecuta viewModel.comenzarEscucha() directamente en su cuerpo. ¿Qué problema puede ocasionar?
  4. ¿Cuál es el propósito de ExistingPeriodicWorkPolicy.KEEP en una programación periódica única?
  5. Un Worker tiene NetworkType.CONNECTED, pero la API devuelve 503. ¿Por qué la restricción no impide esa ejecución y qué respuesta resulta razonable?
  6. Una app utiliza notify(42, aviso) dos veces con el mismo canal e identificador. ¿Se apilan normalmente dos notificaciones o se actualiza la existente?

C. Aplicación y diagnóstico

  1. Diseña una estrategia para mostrar en una lista los cambios de estado de pedidos mientras el usuario consulta la pantalla, minimizando las conexiones innecesarias.
  2. Una aplicación necesita revisar cambios cada cierto tiempo mientras no se utiliza. ¿Cómo la diseñarías con WorkManager y qué garantías de tiempo evitarías prometer?
  3. El usuario instala la app y existen cien registros anteriores. ¿Qué política seguirías para evitar cien notificaciones iniciales?
  4. La app pasa a segundo plano y sigue registrando eventos de una conexión SSE. Identifica una causa probable y propone una corrección.
  5. Un token de acceso vence mientras la aplicación mantiene un stream SSE. ¿Qué debe hacer el cliente para restablecer una sesión válida y evitar reintentos infinitos?
  6. El Worker guarda un cursor antes de mostrar una notificación y el proceso termina en medio. ¿Qué inconsistencia podría producirse y cómo se mitiga?

12. Respuestas posibles

  1. En pull el cliente solicita los datos, como un GET al tocar «Actualizar». En push el servidor entrega un evento sobre un canal previamente establecido, como una conexión SSE.
  2. Porque las corrutinas tienen un ámbito y Android administra la vida del proceso y la ejecución en segundo plano. Para trabajo diferible que debe persistir se emplean mecanismos como WorkManager.
  3. id identifica el evento; event describe su tipo; data contiene la información; la línea en blanco marca el final del evento.
  4. Indica el último evento conocido por el cliente. El servidor debe conservar eventos y admitir la recuperación a partir de ese identificador; si no, pueden existir pérdidas.
  5. callbackFlow convierte callbacks en un flujo; trySend envía valores desde el listener; awaitClose permite liberar la conexión y desregistrar callbacks cuando deja de observarse.
  6. LaunchedEffect se relaciona con composición y claves; LifecycleStartEffect ejecuta una operación durante el estado iniciado del propietario y la limpia en STOP o al desecharse. Una pantalla puede estar compuesta sin estar visible.
  7. Un canal válido cuando la plataforma lo requiere, permiso de notificaciones en las versiones aplicables, configuración de la notificación y un PendingIntent si se quiere ejecutar una acción al tocarla.
  8. WorkManager ejecuta trabajo periódico diferible, sujeto a restricciones, cuotas y ahorro energético. No se ejecuta necesariamente en un instante exacto ni mantiene un canal de push.
  9. El listener o el socket pueden permanecer activos después de terminar el consumo, desperdiciando red y batería, si no se liberan por otro mecanismo.
  10. El cliente puede cerrar la conexión por inactividad antes de recibir el siguiente heartbeat. Conviene configurar un tiempo de espera compatible con el ritmo de datos o latidos.
  11. El cuerpo de un Composable puede ejecutarse repetidamente por recomposiciones, por lo que podría crear varias conexiones. Se usa un efecto o mecanismo ligado a ciclo de vida con limpieza explícita.
  12. Evita sustituir o volver a programar un trabajo periódico con el mismo nombre si uno ya existe; así se limita la duplicación de trabajos.
  13. CONNECTED solo verifica la conectividad disponible, no la salud de la API. Un 503 normalmente amerita un reintento diferido, sujeto a política de recuperación.
  14. Normalmente se actualiza o reemplaza la notificación con el mismo identificador y etiqueta. Esto no garantiza deduplicación de eventos dentro de la aplicación.
  15. Puede abrirse un SSE al llegar a STARTED, transformar los eventos a un Flow y actualizar un estado observable. Al pasar a STOP se cancela la recolección y se cierra la conexión; al regresar se reconecta con un cursor.
  16. Programaría un CoroutineWorker periódico único con restricciones de conectividad, recuperación ante errores transitorios y un cursor persistente. Aclararía que el intervalo mínimo de 15 minutos no representa una ejecución exacta.
  17. En la primera sincronización se puede establecer un cursor inicial sin notificar el historial. Las siguientes revisiones comparan eventos posteriores, según las preferencias y reglas del producto.
  18. Probablemente la recolección está ligada únicamente a la composición, o falta liberar el EventSource. Se puede utilizar LifecycleStartEffect o recolección consciente del ciclo de vida y cancelar el origen en awaitClose.
  19. Debe cerrar o dejar finalizar el stream vencido, renovar credenciales mediante el flujo de autenticación previsto y reconectar con el último evento confirmado. Si el refresh falla permanentemente, se abandona la sesión o se solicita autenticación, evitando ciclos infinitos.
  20. El cursor podría avanzar aunque la notificación nunca se haya publicado, omitiendo ese evento en una ejecución posterior. Se mitiga con identificadores estables, estado de procesamiento persistente y un orden cuidadosamente diseñado, aceptando las limitaciones de coordinación entre almacenamiento y sistema de notificaciones.

13. Síntesis para el examen

El criterio fundamental es asociar cada tarea con su necesidad de actualización y con el componente del sistema que puede sostenerla. Una conexión SSE sirve para recibir información mientras existe un consumidor activo. callbackFlow integra eventos de una biblioteca de callbacks con las corrutinas y awaitClose libera recursos. Los efectos conscientes del ciclo de vida permiten conectar y desconectar de acuerdo con la visibilidad. Las notificaciones son un mecanismo de presentación al usuario y requieren canales, permisos y acciones apropiadas. WorkManager ejecuta consultas diferibles persistentes, con restricciones y sin garantías de puntualidad.

Antes de implementar una solución, conviene poder responder cinco preguntas: ¿cuánto puede tardar la actualización?, ¿la aplicación necesita escuchar o consultar?, ¿qué ocurre cuando deja de estar visible?, ¿cómo se recuperan eventos no recibidos? y ¿cómo se evitan acciones o notificaciones duplicadas?

Referencias para profundizar

  • Android Developers. Lifecycle in Jetpack Compose. https://developer.android.com/topic/libraries/architecture/lifecycle
  • Android Developers. Notification runtime permission. https://developer.android.com/develop/ui/compose/notifications/notification-permission
  • Android Developers. WorkManager. https://developer.android.com/topic/libraries/architecture/workmanager
  • Kotlin Documentation. Callback flows. https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/callback-flow.html
  • MDN Web Docs. Using server-sent events. https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events