¿Cómo programas una sincronización en segundo plano confiable — WorkManager en Android vs BGTaskScheduler en iOS?
La ejecución en segundo plano en móviles es una negociación con el sistema operativo, no una garantía — así decide cada plataforma cuándo tu sincronización realmente se ejecuta.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo programas una sincronización en segundo plano confiable — WorkManager en Android vs BGTaskScheduler en iOS?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Ya nadie tiene un hilo real en segundo plano
Ambas plataformas principales pasaron la última década cerrando el resquicio por el cual una app podía mantener un servicio vivo indefinidamente para hacer polling a un servidor, subir logs o refrescar un feed. Android tiene Doze y los App Standby buckets, que restringen el acceso a red y postergan trabajos cuanto menos hayas usado una app recientemente; iOS nunca prometió CPU ilimitada en segundo plano y en cambio entrega ventanas de ejecución breves y oportunistas que predice que realmente vas a aprovechar bien. Si preguntas "¿cómo ejecuto esto cada 15 minutos para siempre?", la respuesta honesta en ambas plataformas es: no le pides al sistema que ejecute tu código en un horario, registras una unidad de trabajo con un conjunto de restricciones, y el sistema decide el momento real según batería, red, estado térmico y los patrones de uso que registra sobre tu app en particular.
Ese cambio de marco es la pregunta entera. Un ingeniero mobile senior no dice "WorkManager corre cada 15 minutos" — dice "WorkManager garantiza que el trabajo eventualmente se ejecuta cuando se cumplen las restricciones, con un piso mínimo de intervalo, pero el momento exacto es opaco y no debería ser crítico para nada sensible al tiempo."
#WorkManager: la cola durable y guiada por restricciones
WorkManager es una capa sobre JobScheduler, una base de datos interna respaldada en SQLite, un fallback con AlarmManager y (para niveles de API sin JobScheduler) un planificador estilo Firebase — tú no eliges nada de eso, eliges comportamiento. Hay dos formas de solicitud: OneTimeWorkRequest para una tarea diferida única y PeriodicWorkRequest para trabajo recurrente, cuyo intervalo mínimo es 15 minutos — cualquier valor menor se recorta silenciosamente a ese piso. El trabajo sobrevive a la muerte del proceso y al reinicio del dispositivo porque se persiste, no se mantiene en memoria, y le adjuntas Constraints (tipo de red, estado de carga, almacenamiento no bajo, batería no baja) para que el worker ni siquiera arranque hasta que el dispositivo esté en un estado donde valga la pena ejecutarlo.
class SyncWorker(context: Context,params: WorkerParameters,) : CoroutineWorker(context, params) {override suspend fun doWork(): Result {return try {val pending = outboxDao.pendingItems()pending.forEach { item -> api.push(item) }Result.success()} catch (e: IOException) {Result.retry()} catch (e: HttpException) {if (e.code() in 500..599) Result.retry() else Result.failure()}}}fun schedulePeriodicSync(context: Context) {val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).setRequiresBatteryNotLow(true).build()val request = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES).setConstraints(constraints).setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS).build()WorkManager.getInstance(context).enqueueUniquePeriodicWork("outbox-sync",ExistingPeriodicWorkPolicy.KEEP,request,)}
Lo que realmente importa aquí es enqueueUniquePeriodicWork con un nombre: sin eso, llamar dos veces a la función de programación (una al iniciar la app, otra tras un cambio de configuración) produce dos jobs periódicos compitiendo entre sí. ExistingPeriodicWorkPolicy.KEEP dice "si este trabajo con nombre ya existe, no lo toques," que es lo que quieres para un job de sincronización cuyas restricciones no cambiaron. Para trabajo que de verdad no puede esperar — un "envía este mensaje ya, incluso en Doze" — WorkManager 2.7+ expone setExpedited(), que pide al sistema un job expedito (o recurre a un foreground service de corta vida en APIs más antiguas) en vez de esperar en la cola normal.
#BGTaskScheduler: ventanas más cortas, y el sistema decide cuándo
iOS divide el trabajo en segundo plano en dos tipos de solicitud bajo un mismo scheduler. BGAppRefreshTaskRequest es para refrescos breves, "rellenar la caché" — segundos, no minutos. BGProcessingTaskRequest es para mantenimiento más largo — limpieza de base de datos, una subida por lotes — y puede declarar requiresNetworkConnectivity y requiresExternalPower. Ambos deben registrarse con BGTaskScheduler.shared.register(forTaskWithIdentifier:using:) antes de que application(_:didFinishLaunchingWithOptions:) retorne, y cada identificador que registres también debe aparecer en BGTaskSchedulerPermittedIdentifiers dentro de Info.plist, o el registro falla silenciosamente en tiempo de ejecución.
// Registrado en application(_:didFinishLaunchingWithOptions:), antes de que retorne.BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.acme.app.sync",using: nil) { task inhandleSync(task: task as! BGProcessingTask)}func scheduleSync() {let request = BGProcessingTaskRequest(identifier: "com.acme.app.sync")request.requiresNetworkConnectivity = truerequest.requiresExternalPower = falserequest.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)do {try BGTaskScheduler.shared.submit(request)} catch {print("No se pudo programar la sincronización: \(error)")}}func handleSync(task: BGProcessingTask) {scheduleSync() // reprograma la próxima ejecución antes de hacer cualquier trabajolet operation = SyncOperation()task.expirationHandler = { operation.cancel() }operation.completionBlock = {task.setTaskCompleted(success: !operation.isCancelled)}OperationQueue().addOperation(operation)}
earliestBeginDate es una pista, no una promesa — le dice al sistema "ni siquiera consideres esto antes de ahora," pero iOS usa predicción de uso en el propio dispositivo para decidir el momento real de ejecución, y en la práctica un BGProcessingTask puede correr una o dos veces al día para una app que el usuario abre con poca frecuencia. El expirationHandler no es opcional en ninguna implementación seria: el sistema te da un presupuesto de tiempo y terminará el proceso si te excedes, así que cancelar limpiamente y llamar a setTaskCompleted desde la ruta de expiración es lo que mantiene al sistema dispuesto a concederte futuras ejecuciones — abusar del presupuesto hace que una app quede excluida por completo de la ejecución en segundo plano.
#Diseñar la sincronización en torno a la incertidumbre
Como ningún scheduler te da un reloj garantizado, la lógica de sincronización tiene que ser idempotente y segura de ejecutar más o menos veces de las solicitadas — un worker que se repite porque el proceso murió a mitad de la ejecución no debe reenviar dos veces el mismo elemento del outbox, lo cual usualmente implica una bandera local de "en curso" o una clave de idempotencia del lado del servidor, en vez de confiar en que "esto solo corre una vez." Para algo que de verdad no puede esperar — un mensaje de chat, la confirmación de un pago — la herramienta correcta no es un horario en segundo plano más ajustado, es una notificación push que despierta la app según el reloj del servidor y no según el del sistema operativo.
🧠 WorkManager vs BGTaskScheduler
0/2 · 0/2 answered1. ¿Cuál es el intervalo mínimo que WorkManager permite para un PeriodicWorkRequest?
2. ¿Qué garantiza realmente earliestBeginDate en BGTaskScheduler?