La guía de decisión
Con tantas opciones, la pregunta es siempre: ¿qué API uso? La respuesta depende de dos variables:
# Pregunta 1: ¿Necesita ejecutarse en un momento EXACTO?
# Sí → AlarmManager (setExactAndAllowWhileIdle o setAlarmClock)
# No → seguir al paso 2
# Pregunta 2: ¿El usuario necesita verlo mientras ocurre?
# Sí (progress visible, no se puede interrumpir) → ForegroundService
# No → seguir al paso 3
# Pregunta 3: ¿Es trabajo diferible que debe completarse eventualmente?
# Sí → WorkManager (OneTimeWorkRequest o PeriodicWorkRequest)
# No → seguir al paso 4
# Pregunta 4: ¿La app está en foreground cuando ocurre?
# Sí → Coroutines con viewModelScope o lifecycleScope
# No → probablemente no necesitás background work
Escenarios reales mapeados
# App de fotos — subir foto después de sacarla
# → WorkManager (OneTime, constraint: CONNECTED)
# Razón: diferible, debe completarse aunque el usuario cierre la app
# App de música — reproducir en background
# → ForegroundService (mediaPlayback)
# Razón: requiere notificación visible, no se puede diferir
# App de delivery — tracking GPS del repartidor
# → ForegroundService (location)
# Razón: requiere acceso continuo a ubicación, notificación visible
# App de recordatorios — notificar a las 9:00 AM
# → AlarmManager (setAlarmClock o setExactAndAllowWhileIdle)
# Razón: el tiempo exacto importa
# App de noticias — actualizar feed cada hora
# → WorkManager (PeriodicWork, constraint: CONNECTED)
# Razón: diferible, el sistema elige el mejor momento
# App de chat — mostrar la respuesta del usuario mientras escribe
# → Coroutines (viewModelScope + StateFlow)
# Razón: la app está en foreground, no necesita background work
# App financiera — actualizar cotizaciones mientras la app está abierta
# → Coroutines (repeatOnLifecycle + Flow con polling)
# Razón: solo necesario cuando el usuario ve la pantalla
# App de fitness — registrar pasos durante el día
# → ForegroundService (health) + WorkManager para el reporte diario
# Razón: combinar ambos según la necesidad
Errores comunes a evitar
// ❌ ERROR 1: Usar GlobalScope para trabajo en background
GlobalScope.launch {
repository.sync() // Leak garantizado — nunca se cancela
}
// ✓ Usar viewModelScope o WorkManager
// ❌ ERROR 2: Hacer trabajo de red en el hilo principal
class MainViewModel : ViewModel() {
init {
// Esto puede lanzar NetworkOnMainThreadException
val datos = apiService.getDatos() // SÍNCRONO en init
}
}
// ✓ Usar viewModelScope.launch { }
// ❌ ERROR 3: Asumir que el Worker terminará en menos de 10 minutos
class HeavyWorker : CoroutineWorker(...) {
override suspend fun doWork(): Result {
procesarMillonesDeRegistros() // El sistema lo cancela después de 10 min
return Result.success()
}
}
// ✓ Partir el trabajo en Workers encadenados más pequeños
// ❌ ERROR 4: No manejar el permiso de alarmas exactas en Android 12+
alarmManager.setExact(...) // Crashea si no tiene el permiso
// ✓ Verificar con canScheduleExactAlarms() antes
// ❌ ERROR 5: Iniciar ForegroundService desde background en Android 12+
// Viene de un Worker o un BroadcastReceiver
context.startForegroundService(intent) // ForegroundServiceStartNotAllowedException
// ✓ Usar WorkManager con expedited work desde background
// ❌ ERROR 6: Guardar contexto de Activity en el Worker
class MiWorker : Worker() {
val activity: Activity? = null // Leak enorme
// ...
}
// ✓ Usar applicationContext dentro del Worker
FCM + WorkManager — el patrón más poderoso
Firebase Cloud Messaging (FCM) + WorkManager es la combinación más robusta para sincronización reactiva:
// FCM despierta la app con un push — incluso en Doze
// WorkManager ejecuta la sincronización con garantía y constraints
class MiMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
val tipo = message.data["tipo"] ?: return
when (tipo) {
"sync_productos" -> {
// Encolar WorkManager — no hacer la sync aquí directamente
val syncRequest = OneTimeWorkRequestBuilder()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(workDataOf("categoria" to message.data["categoria"]))
.build()
WorkManager.getInstance(applicationContext).enqueue(syncRequest)
}
"nuevo_mensaje" -> {
// Mostrar notificación directamente para mensajes
mostrarNotificacion(message)
}
}
}
}
Por qué esta combinación es la mejorFCM garantiza que el push llega incluso en Doze (usa conexiones de alta prioridad del sistema). WorkManager garantiza que la sincronización se completa aunque el proceso muera durante la ejecución. Juntos cubren todos los casos de falla posibles.
Debugging de tareas en background
# Forzar salida de Doze para testing:
adb shell dumpsys deviceidle force-idle
adb shell dumpsys deviceidle unforce
# Simular que la batería está baja:
adb shell am broadcast -a android.intent.action.BATTERY_LOW
# Ver estado de App Standby:
adb shell am get-standby-bucket ar.pensa.miapp
# Forzar un bucket específico:
adb shell am set-standby-bucket ar.pensa.miapp active
adb shell am set-standby-bucket ar.pensa.miapp rare
# Ver workers de WorkManager en ejecución:
adb shell dumpsys jobscheduler | grep -A5 "ar.pensa.miapp"
# Android Studio → App Inspection → WorkManager:
# Ver todos los workers, su estado y sus datos