Actualizaciones en tiempo real, notificaciones y trabajo en segundo plano en Android
SSE, callbackFlow, ciclo de vida, notificaciones y WorkManager
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
- ¿Qué diferencia hay entre obtener cambios mediante pull y recibirlos mediante push? Proporciona un ejemplo de cada uno.
- ¿Por qué una corrutina iniciada desde una pantalla no garantiza la recepción de novedades con la aplicación cerrada?
- Describe la estructura básica de un mensaje SSE y explica el papel de
id,event,datay la línea en blanco. - ¿Qué información aporta
Last-Event-ID? ¿Qué requisito adicional debe satisfacer el servidor para recuperar eventos perdidos? - ¿Qué función cumplen
callbackFlow,trySendyawaitCloseen una conexión prolongada? - Explica por qué
LaunchedEffectyLifecycleStartEffectno son intercambiables en todos los casos. - ¿Qué elementos intervienen en la publicación y apertura de una notificación Android?
- ¿Por qué una tarea periódica de WorkManager no equivale a una notificación instantánea?
B. Interpretación de código
- En un
callbackFlowse crea unEventSource, pero no se registra su cancelación enawaitClose. ¿Qué riesgo aparece al abandonar la pantalla? - Un servidor envía un heartbeat cada 20 segundos y el cliente tiene
readTimeout(10, TimeUnit.SECONDS). ¿Qué comportamiento cabe esperar? - Un Composable ejecuta
viewModel.comenzarEscucha()directamente en su cuerpo. ¿Qué problema puede ocasionar? - ¿Cuál es el propósito de
ExistingPeriodicWorkPolicy.KEEPen una programación periódica única? - Un Worker tiene
NetworkType.CONNECTED, pero la API devuelve503. ¿Por qué la restricción no impide esa ejecución y qué respuesta resulta razonable? - 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
- 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.
- 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?
- El usuario instala la app y existen cien registros anteriores. ¿Qué política seguirías para evitar cien notificaciones iniciales?
- La app pasa a segundo plano y sigue registrando eventos de una conexión SSE. Identifica una causa probable y propone una corrección.
- 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?
- 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
- En pull el cliente solicita los datos, como un
GETal tocar «Actualizar». En push el servidor entrega un evento sobre un canal previamente establecido, como una conexión SSE. - 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.
ididentifica el evento;eventdescribe su tipo;datacontiene la información; la línea en blanco marca el final del evento.- 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.
callbackFlowconvierte callbacks en un flujo;trySendenvía valores desde el listener;awaitClosepermite liberar la conexión y desregistrar callbacks cuando deja de observarse.LaunchedEffectse relaciona con composición y claves;LifecycleStartEffectejecuta 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.- Un canal válido cuando la plataforma lo requiere, permiso de notificaciones en las versiones aplicables, configuración de la notificación y un
PendingIntentsi se quiere ejecutar una acción al tocarla. - 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.
- 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.
- 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.
- 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.
- 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.
CONNECTEDsolo verifica la conectividad disponible, no la salud de la API. Un503normalmente amerita un reintento diferido, sujeto a política de recuperación.- 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.
- Puede abrirse un SSE al llegar a STARTED, transformar los eventos a un
Flowy 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. - Programaría un
CoroutineWorkerperió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. - 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.
- Probablemente la recolección está ligada únicamente a la composición, o falta liberar el
EventSource. Se puede utilizarLifecycleStartEffecto recolección consciente del ciclo de vida y cancelar el origen enawaitClose. - 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.
- 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