¿Cómo optimizas el trabajo en segundo plano para la batería — modo Doze, restricciones de WorkManager, JobScheduler?
Android limita agresivamente el trabajo en segundo plano una vez que el dispositivo está inactivo, así que la habilidad real no es pelear contra Doze — es declarar restricciones lo bastante precisas para que el sistema programe tu trabajo gratis.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo optimizas el trabajo en segundo plano para la batería — modo Doze, restricciones de WorkManager, JobScheduler?
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.#Por qué la plataforma pelea contigo a propósito
Cada servicio en segundo plano, bucle de polling o WakeLock parcial que una app mantiene mantiene despiertos a la CPU y a la radio, y despertar la radio es desproporcionadamente caro — llevar una radio celular o WiFi de inactiva a activa cuesta mucha más energía que los pocos kilobytes de datos que luego transfiere, un efecto conocido comúnmente como "tail energy" (energía de cola). La respuesta de Android, empezando en Marshmallow y afilándose en cada versión desde entonces, es dejar de confiar en que las apps se autorregulen y en su lugar diferir, agrupar y limitar el trabajo en segundo plano a nivel del sistema operativo. Entender ese cambio — de "las apps se programan a sí mismas" a "las apps declaran lo que necesitan y el sistema operativo decide cuándo" — es todo el modelo mental detrás de Doze, App Standby y WorkManager.
#Doze y App Standby: dos limitadores independientes
El modo Doze se activa a nivel de todo el dispositivo cuando la pantalla está apagada, el dispositivo está desconectado de la corriente y ha estado quieto durante un tiempo. Pasa por intervalos de "siesta" cada vez más largos, interrumpidos por breves ventanas de mantenimiento; fuera de esas ventanas, el acceso a la red se suspende, la mayoría de los wakelocks se ignoran, las alarmas de AlarmManager se difieren (setExact/setExactAndAllowWhileIdle son las excepciones acotadas para alarmas genuinamente críticas en tiempo y visibles para el usuario), y los trabajos de JobScheduler/WorkManager esperan a la siguiente ventana. Los buckets de App Standby son un limitador independiente, por app, introducido en Android 9: según qué tan reciente y frecuentemente un usuario realmente interactúa con una app, el sistema la coloca en ACTIVE, WORKING_SET, FREQUENT, RARE o RESTRICTED, y cada bucket limita con qué frecuencia se permite ejecutar los trabajos de esa app específica y sus mensajes FCM de alta prioridad — independientemente de si el dispositivo está en Doze o no. Una app que el usuario abre a diario recibe muchísima más libertad en segundo plano que una que instaló y nunca volvió a abrir.
#JobScheduler vs. WorkManager
JobScheduler (API 21+) es la API a nivel de plataforma para trabajo diferido basado en restricciones: describes un JobInfo con restricciones — tipo de red, estado de carga, dispositivo inactivo — y el sistema agrupa tu trabajo junto con el de otras apps para minimizar cuántas veces tiene que despertar la radio en total. Es poderosa pero tiene dos limitaciones reales que aparecen constantemente en la práctica: no existe antes de API 21, y las garantías de persistencia de un trabajo frente a la muerte del proceso o un reinicio son fáciles de implementar mal a mano. WorkManager es la respuesta de Jetpack — se apoya en JobScheduler en niveles de API modernos y cae de vuelta a AlarmManager + un BroadcastReceiver en los más antiguos, persiste cada solicitud encolada en una base de datos Room interna para que el trabajo sobreviva a la muerte del proceso e incluso a un reinicio, y te da una API consistente sin importar cuál backend esté corriendo en realidad. A menos que tengas una razón muy específica para implementar JobScheduler a mano (normalmente: necesitar una funcionalidad de plataforma que WorkManager aún no envuelve), WorkManager es la respuesta por defecto tanto en una entrevista como en la práctica.
#Restricciones, cadencia, y la honestidad que imponen
Un objeto Constraints — setRequiredNetworkType, requiresCharging, requiresBatteryNotLow, requiresStorageNotLow, requiresDeviceIdle — se evalúa como una conjunción: todas deben cumplirse antes de que el sistema siquiera intente ejecutar el trabajo, y si una restricción deja de cumplirse a mitad de la ejecución, el trabajo se detiene y se reprograma. Esta es la palanca real que un entrevistador está buscando: en lugar de escribir tu propio bucle de polling y esperar que se comporte bien, declaras exactamente qué condiciones hacen aceptable el trabajo, y el sistema — que también ve las restricciones de todas las demás apps — decide el momento más barato para satisfacerlas todas a la vez, muchas veces agrupando el trabajo de red de varias apps en un solo despertar de radio. PeriodicWorkRequest tiene un piso duro de 15 minutos entre ejecuciones sin importar qué configures por debajo de eso, específicamente para evitar que las apps conviertan "periódico" en "constante". Cuando genuinamente necesitas que algo corra pronto y no puedes esperar a una ventana de mantenimiento de Doze, setExpedited() (WorkManager 2.7+) solicita programación prioritaria — respaldada por los expedited jobs de JobScheduler en API 31+, o un servicio en primer plano de corta duración por debajo de eso — sujeta a una cuota a nivel de sistema, así que es para momentos de "el usuario acaba de tocar enviar", no un sustituto de una programación real.
#Cómo se ve esto para una app de feed en concreto
El hábito de batería de mayor impacto para una app de contenido es agrupar (batching): sincronizar un lote de cambios pendientes en una sola solicitud dentro de un trabajo periódico con requiresBatteryNotLow y UNMETERED cuando sea posible, en lugar de hacer muchas llamadas pequeñas a lo largo del día, ya que el ramp-up de radio de cada llamada eclipsa a la transferencia en sí. Difiere todo lo que no sea urgente (precarga de imágenes para la sesión de mañana, subida de analítica, calentamiento de caché) a requiresCharging = true combinado con UNMETERED, para que solo corra cuando el usuario está conectado a la corriente y fuera de datos móviles — gratis desde la perspectiva del usuario. Reserva la entrega en tiempo real para FCM en lugar de hacer polling; un push de alta prioridad puede despertar brevemente la app incluso durante Doze, pero depender demasiado de eso hace que App Standby termine limitando la app, así que debería reservarse para eventos genuinamente sensibles al tiempo, no para sincronización rutinaria. Y siempre combina setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ...) con trabajo reintentable — un reintento ingenuo, lineal o inmediato, ante una red inestable es exactamente el comportamiento de bucle caliente que Doze y App Standby existen para frenar.
class FeedSyncWorker(context: Context,params: WorkerParameters,) : CoroutineWorker(context, params) {override suspend fun doWork(): Result = try {val pendingChanges = feedRepository.pullPendingChanges()feedApi.syncBatch(pendingChanges) // una sola solicitud para todo el lote, no una por cambioResult.success()} catch (e: IOException) {Result.retry() // fallo de red transitorio — deja que el backoff maneje la espera} catch (e: HttpException) {if (e.code() in 500..599) Result.retry() else Result.failure()}}// Sincronización rutinaria: agrupada, diferida, solo cuando no le cuesta nada al usuario.val syncConstraints = Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED).setRequiresBatteryNotLow(true).build()val periodicSync = PeriodicWorkRequestBuilder<FeedSyncWorker>(repeatInterval = 15, TimeUnit.MINUTES, // el piso del sistema operativo — no se puede bajar).setConstraints(syncConstraints).setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS).build()WorkManager.getInstance(context).enqueueUniquePeriodicWork("feed-background-sync",ExistingPeriodicWorkPolicy.KEEP,periodicSync,)// "El usuario acaba de publicar" — urgente pero breve, vale la pena gastar cuota expedited.val expeditedUpload = OneTimeWorkRequestBuilder<FeedSyncWorker>().setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()).build()WorkManager.getInstance(context).enqueue(expeditedUpload)
🧠 Repaso de Doze y WorkManager
0/3 · 0/3 answered1. Una app programa un PeriodicWorkRequest con un repeatInterval de 5 minutos. ¿Qué sucede en realidad?
2. ¿Por qué generalmente se prefiere WorkManager sobre llamar directamente a JobScheduler?
3. ¿Por qué agrupar diez llamadas de sincronización pequeñas en una sola solicitud suele ahorrar más batería de lo que sugeriría solo el conteo de bytes?