Elegir la herramienta: el árbol de decisión completo
Corrutina, WorkManager, servicio en primer plano, AlarmManager o mensaje push: cinco mecanismos que resuelven problemas distintos y que se confunden constantemente porque todos parecen hacer que algo ocurra más tarde. Esta lección construye el árbol de decisión definitivo a partir de tres preguntas ordenadas sobre el origen del trabajo, su tolerancia al retraso y su necesidad de sobrevivir al proceso, recorre las cinco herramientas con sus garantías y sus costes reales, enumera los errores de emparejamiento más caros con su síntoma característico, y cierra con los patrones compuestos donde varias herramientas colaboran en una misma funcionalidad.
Todo lo aprendido en este nivel converge en una única decisión práctica que se toma docenas de veces a lo largo de un proyecto y que casi nadie toma explícitamente. Alguien necesita que algo ocurra fuera de la pantalla actual, abre el editor y escribe lo primero que le viene a la cabeza, que suele ser lanzar una corrutina porque está a mano. El código funciona en el emulador, pasa la revisión porque nadie hace la pregunta correcta, y meses después aparece un informe de que a algunos usuarios se les pierden las cosas. La elección de mecanismo no es un detalle de implementación intercambiable: cada una de las cinco herramientas ofrece un conjunto distinto de garantías y cobra un precio distinto en batería, en visibilidad y en permisos. Elegir bien no requiere experiencia, requiere hacer tres preguntas en el orden correcto antes de escribir la primera línea.
- Aplicar las tres preguntas que determinan el mecanismo correcto antes de escribir código.
- Enunciar la garantía exacta y el coste real de cada una de las cinco herramientas.
- Reconocer por su síntoma los errores de emparejamiento más frecuentes y corregirlos.
- Componer varias herramientas en una misma funcionalidad cuando ninguna basta por sí sola.
Las tres preguntas que deciden todo
La primera pregunta es quién origina el trabajo y si el usuario está mirando. Si el trabajo nace de un toque y su resultado se muestra en la pantalla que el usuario tiene delante, la respuesta está cerrada antes de empezar: es una corrutina en el ámbito de la pantalla, y cualquier otra cosa es sobreingeniería. Si el trabajo nace de un toque pero el usuario espera que continúe mientras hace otra cosa, es un servicio en primer plano. Si nace del servidor, es un mensaje push. Si nace de un momento del calendario que el usuario fijó, es una alarma.
La segunda pregunta es cuánto retraso tolera. Aquí conviene ser brutalmente honesto, porque la respuesta instintiva de cualquiera es que su tarea es urgente y casi nunca lo es. La prueba de fuego consiste en imaginar que la tarea se ejecuta seis horas tarde, mientras el teléfono carga de madrugada, y preguntarse qué pierde el usuario exactamente. Si la respuesta es nada apreciable, la tarea es diferible y pertenece al planificador, con sus condiciones y sus reintentos.
La tercera pregunta es si el trabajo debe sobrevivir a la muerte del proceso y al reinicio del dispositivo. Esta pregunta es la que descarta la corrutina para todo lo que importa. Una corrutina es memoria de tu proceso: cuando el sistema recicla el proceso, desaparece sin dejar rastro y sin ejecutar nada. Si perder ese trabajo a mitad de camino produce un estado incorrecto o una promesa incumplida al usuario, la corrutina está descartada, con independencia de lo cómoda que resulte.
Corrutina
Trabajo ligado a una pantalla viva. Cero garantías fuera de ella. Muere con el proceso sin avisar.
WorkManager
Trabajo diferible con garantía de ejecución. Sobrevive al proceso y al reinicio. Retraso variable por diseño.
Servicio en primer plano
Trabajo continuo iniciado por el usuario. Coste: notificación visible, tipo declarado y permisos por tipo.
AlarmManager
Un instante concreto del calendario del usuario. Las alarmas exactas exigen permiso y justificación.
Las cinco herramientas y lo que garantiza cada una
La corrutina garantiza únicamente concurrencia dentro de un proceso vivo. Es la herramienta correcta para cargar una pantalla, ejecutar una búsqueda mientras el usuario escribe o guardar un borrador local. Su virtud es el coste nulo y la cancelación estructurada; su límite es absoluto y no admite matices. Un ámbito de aplicación que sobreviva a la pantalla no arregla nada, porque el proceso sigue siendo mortal.
WorkManager garantiza que el trabajo se ejecutará eventualmente, incluso si el proceso muere o el dispositivo se reinicia, respetando las condiciones que declares y reintentando con retroceso exponencial ante fallo. Es la herramienta por defecto para casi todo lo que no es interactivo: sincronizar, subir, procesar, limpiar. Su coste es que no controlas el momento, y su virtud principal es que sus trabajos únicos evitan duplicaciones y que su cadena de dependencias permite componer secuencias complejas sin inventar orquestación propia.
El servicio en primer plano garantiza ejecución continua mientras la notificación esté visible, con la reserva de los topes temporales de ciertos tipos. El mensaje push garantiza entrega desde el servidor incluso durante Doze cuando se envía con prioridad alta, y concede una ventana breve de privilegio al recibirse, lo que lo convierte en el detonante correcto de cualquier trabajo urgente originado fuera del dispositivo. La alarma garantiza un disparo en un instante determinado, con exactitud si se pide el permiso correspondiente y de forma aproximada en caso contrario.
// Diferible con condiciones y reintento: el caso mayoritario
val sync = PeriodicWorkRequestBuilder<SincronizarWorker>(12, TimeUnit.HOURS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build(),
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(contexto)
.enqueueUniquePeriodicWork("sync", ExistingPeriodicWorkPolicy.UPDATE, sync)
// Urgente originado por el servidor: el push detona, el worker ejecuta
class MensajeriaService : FirebaseMessagingService() {
override fun onMessageReceived(mensaje: RemoteMessage) {
WorkManager.getInstance(this).enqueue(
OneTimeWorkRequestBuilder<ProcesarWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(workDataOf("id" to mensaje.data["id"]))
.build(),
)
}
}
flowchart TD
A[Hay trabajo que hacer] --> B{El usuario esta mirando el resultado ahora}
B -->|si| C[Corrutina en el ambito de la pantalla]
B -->|no| D{Quien lo origina}
D -->|el servidor| E[Mensaje push de prioridad alta]
D -->|un instante del calendario| F[AlarmManager con permiso si es exacta]
D -->|el usuario y debe continuar visible| G[Servicio en primer plano con tipo]
D -->|nada externo, es rutina| H{Tolera horas de retraso}
H -->|si| I[WorkManager con condiciones]
H -->|no| J[Revisar el diseno: casi siempre si tolera]
E --> I
style C fill:#a6e3a1,color:#11111b
style I fill:#a6e3a1,color:#11111b
style J fill:#f9e2af,color:#11111bLos errores de emparejamiento y su síntoma
Cada error tiene una firma reconocible, y aprender a leerla ahorra semanas de depuración. La corrutina usada para trabajo que debía persistir produce pérdidas intermitentes que nadie reproduce: el usuario dice que a veces no se sube la foto, y no hay ningún error en los registros porque no hubo error, hubo un proceso reciclado. El síntoma delator es que la incidencia correlaciona con teléfonos de poca memoria.
El servicio en primer plano usado para trabajo diferible produce quejas de batería, notificaciones que el usuario no entiende y, desde las versiones recientes, detenciones inesperadas al agotarse el tope del tipo declarado. El síntoma delator es una notificación cuyo texto no describe nada que el usuario haya pedido.
La alarma exacta usada para sincronizar produce rechazo en la tienda y consumo desproporcionado, porque cada disparo despierta el dispositivo fuera de las ventanas agrupadas. El planificador usado para algo que el usuario espera al instante produce la queja de que la aplicación es lenta o de que las cosas aparecen tarde. Y el push usado para transportar datos en lugar de para avisar produce pérdidas silenciosas, porque la entrega no está garantizada en el tiempo ni en el orden y los mensajes pueden colapsarse entre sí.
El error de arquitectura más común en aplicaciones conectadas consiste en meter la carga útil dentro del mensaje push y actuar sobre ella directamente. El servicio de mensajería no garantiza entrega ordenada, puede fusionar mensajes pendientes de un mismo tema y tiene límites de tamaño. El patrón robusto es el opuesto: el push transporta únicamente un identificador y significa hay novedades, y el trabajo real consiste en consultar al servidor el estado actual. Así, perder tres mensajes de cuatro no produce ninguna pérdida de información, porque la cuarta consulta trae todo lo acumulado. Un sistema construido sobre esta regla es indiferente a la fiabilidad del canal.
Patrones compuestos: cuando ninguna basta sola
Las funcionalidades reales rara vez encajan en una sola herramienta, y los patrones que funcionan tienen forma de cadena donde cada eslabón aporta la garantía que falta.
La subida de un archivo grande que el usuario acaba de elegir empieza como servicio en primer plano mientras el usuario mira el progreso, y si el trabajo excede lo razonable o el usuario abandona, se convierte en trabajo diferido con reintento. En la práctica se implementa directamente como trabajo largo con notificación, que es la forma que ofrece el planificador para promocionar su propia ejecución a primer plano y obtener lo mejor de ambos mundos.
La mensajería instantánea combina push de prioridad alta como detonante, trabajo expedito para consultar el servidor y notificación con intención pendiente para pedir la atención del usuario sin robársela. El recordatorio del usuario combina alarma exacta con permiso para el instante, notificación de pantalla completa si es una alarma de despertador y trabajo diferido para la limpieza posterior. La sincronización periódica es trabajo declarado con condiciones, sin ninguna otra pieza.
// Trabajo largo que se promociona a primer plano por si mismo
class SubidaWorker(ctx: Context, params: WorkerParameters) :
CoroutineWorker(ctx, params) {
override suspend fun getForegroundInfo() = ForegroundInfo(
ID_AVISO,
construirNotificacion(progreso = 0),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
override suspend fun doWork(): Result {
setForeground(getForegroundInfo())
return try {
repositorio.subir(inputData.getString("uri")!!) { hecho ->
setProgress(workDataOf("progreso" to hecho))
}
Result.success()
} catch (e: IOException) {
Result.retry()
}
}
}
Conviene cerrar el nivel con la reformulación que vuelve trivial una decisión que parece complicada. Las cinco herramientas de esta lección no son cinco maneras alternativas de ejecutar código; son cinco contratos distintos, y un contrato se define por lo que garantiza y por lo que cobra. La corrutina no garantiza nada y no cobra nada. El planificador garantiza ejecución eventual y cobra impredecibilidad en el momento. El servicio en primer plano garantiza continuidad y cobra visibilidad permanente ante el usuario, más un tipo declarado y sus permisos. La alarma exacta garantiza un instante y cobra un permiso que la plataforma vigila y que la tienda revisa. El push garantiza un aviso desde fuera y cobra una dependencia de un servicio externo y una entrega que no es fiable individualmente. Vista así, la pregunta de qué herramienta usar se disuelve en otra mucho más fácil de responder honestamente: qué garantía necesito de verdad, y estoy dispuesto a pagar su precio. La mayoría de los errores de este terreno no son errores de conocimiento sino de honestidad en esa segunda pregunta, porque casi todo el mundo cree que su trabajo necesita ejecutarse ahora, y casi ningún trabajo lo necesita. Hay además una asimetría que conviene interiorizar. Comprar más garantía de la necesaria tiene un coste acotado y visible: gastas batería, molestas al usuario con una notificación que no entiende, pides un permiso que quizá te nieguen. Comprar menos garantía de la necesaria tiene un coste ilimitado e invisible: el sistema funciona perfectamente durante meses y falla en una fracción de dispositivos, en momentos que nadie puede reproducir, produciendo informes que no se parecen a la causa. Por eso, ante la duda genuina entre dos niveles contiguos del árbol, la elección prudente es el superior, y por eso la corrutina, que es la opción por defecto de casi todos los desarrolladores, es también la fuente de la mayor parte de los fallos silenciosos de esta categoría. Y por encima de cualquier tabla comparativa queda el criterio que sobrevivirá a todas las versiones futuras de Android: pregunta siempre qué le habrías prometido al usuario si te hubiera preguntado en voz alta qué va a pasar con lo que acaba de hacer, y compra exactamente la garantía que hace verdadera esa promesa.
- Haz el inventario completo de cada operación asíncrona de tu aplicación y anota para cada una qué herramienta usa hoy y qué garantía necesita en realidad.
- Localiza cada corrutina que sobreviva a la pantalla que la lanzó y decide si es un fallo latente o un caso genuinamente inocuo, justificando cada veredicto.
- Toma tu sincronización más importante y reescríbela con condiciones, trabajo único y retroceso exponencial, comprobando que un reinicio del dispositivo no la pierde.
- Convierte un servicio en primer plano existente en trabajo largo con notificación y mide la diferencia de comportamiento bajo
Dozey con la aplicación en el cubo raro. - Revisa tu integración de push y comprueba si transporta datos o solo avisa. Si transporta datos, reescríbela como campana e implementa la consulta de estado.