PeriodicWorkRequest — trabajo repetido
Para tareas que deben ejecutarse periódicamente (sync cada hora, backup diario):
// Mínimo intervalo: 15 minutos (limitación del sistema)
val syncPeriodico = PeriodicWorkRequestBuilder(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
// Encolar el trabajo periódico con nombre único
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync_periodico",
ExistingPeriodicWorkPolicy.KEEP, // no reiniciar el timer si ya existe
syncPeriodico
)
// Con flex period — ejecutar en una ventana de tiempo
// "Ejecutar una vez por hora, preferiblemente en los últimos 15 minutos"
val syncConFlex = PeriodicWorkRequestBuilder(
repeatInterval = 1, TimeUnit.HOURS,
flexTimeInterval = 15, TimeUnit.MINUTES
).build()
El intervalo no es exactoWorkManager puede diferir la ejecución por Doze, App Standby o constraints no cumplidos. Un worker configurado para cada 15 minutos puede correr cada 20-30 minutos en la práctica. Si necesitás exactitud al minuto, usá AlarmManager (lección 06).
Expedited work — prioridad alta
Para trabajo urgente que debe ejecutarse lo antes posible pero sin requerir un ForegroundService:
// Expedited work corre incluso cuando la app está en background
// y con mayor prioridad que el trabajo normal
val uploadRequest = OneTimeWorkRequestBuilder()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
// OutOfQuotaPolicy.DROP_WORK_REQUEST — si no hay cuota disponible, descartar
.build()
WorkManager.getInstance(context).enqueue(uploadRequest)
// El Worker también debe declarar que es expedited:
class UploadWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
// Obligatorio para expedited work en Android 12+
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = crearNotificacion("Subiendo archivo...")
return ForegroundInfo(NOTIFICATION_ID, notification)
}
override suspend fun doWork(): Result {
// ...
return Result.success()
}
}
Chaining — encadenar Workers
Workers pueden encadenarse para crear pipelines de procesamiento:
// Pipeline: comprimir → subir → notificar
WorkManager.getInstance(context)
.beginWith(OneTimeWorkRequestBuilder().build())
.then(OneTimeWorkRequestBuilder().build())
.then(OneTimeWorkRequestBuilder().build())
.enqueue()
// Paralelismo — múltiples workers en paralelo, luego merge
val compresionFotos = OneTimeWorkRequestBuilder().build()
val compresionVideos = OneTimeWorkRequestBuilder().build()
val upload = OneTimeWorkRequestBuilder().build()
WorkManager.getInstance(context)
.beginWith(listOf(compresionFotos, compresionVideos)) // en paralelo
.then(upload) // espera a que ambos terminen
.enqueue()
// Si algún worker falla, los siguientes NO se ejecutan (a menos que uses APPEND_OR_REPLACE)
Reportar progreso desde el Worker
class UploadWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val archivos = obtenerArchivos()
archivos.forEachIndexed { index, archivo ->
// Reportar progreso
val progreso = (index + 1) * 100 / archivos.size
setProgress(workDataOf("progreso" to progreso))
subirArchivo(archivo)
}
return Result.success()
}
}
// Observar el progreso en el Fragment/Activity:
WorkManager.getInstance(context)
.getWorkInfosByTagLiveData("upload")
.observe(viewLifecycleOwner) { workInfoList ->
workInfoList?.forEach { workInfo ->
val progreso = workInfo.progress.getInt("progreso", 0)
binding.progressBar.progress = progreso
}
}
Observar estado del trabajo
// Observar por ID (el más específico)
val workRequest = OneTimeWorkRequestBuilder().build()
WorkManager.getInstance(context).enqueue(workRequest)
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(workRequest.id)
.observe(viewLifecycleOwner) { workInfo ->
when (workInfo?.state) {
WorkInfo.State.ENQUEUED -> mostrar("En cola...")
WorkInfo.State.RUNNING -> mostrar("Ejecutando...")
WorkInfo.State.SUCCEEDED -> {
val count = workInfo.outputData.getInt("count", 0)
mostrar("Completado: $count elementos")
}
WorkInfo.State.FAILED -> {
val error = workInfo.outputData.getString("error")
mostrarError("Error: $error")
}
WorkInfo.State.CANCELLED -> mostrar("Cancelado")
WorkInfo.State.BLOCKED -> mostrar("Esperando...")
null -> {}
}
}
// Con Flow (más moderno):
WorkManager.getInstance(context)
.getWorkInfoByIdFlow(workRequest.id)
.collectLatest { workInfo -> /* ... */ }
Cancelar trabajo
val workManager = WorkManager.getInstance(context)
// Por ID
workManager.cancelWorkById(workRequest.id)
// Por tag
workManager.cancelAllWorkByTag("sync_productos")
// Por nombre único
workManager.cancelUniqueWork("sync_periodico")
// Todo el trabajo pendiente (con cuidado)
workManager.cancelAllWork()
Testing de Workers
// build.gradle (app)
androidTestImplementation("androidx.work:work-testing:2.9.0")
// Test de un Worker:
@RunWith(AndroidJUnit4::class)
class SyncWorkerTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
// Inicializar WorkManager en modo test
WorkManagerTestInitHelper.initializeTestWorkManager(context)
}
@Test
fun syncWorker_onSuccess_returnsSuccess() {
val inputData = workDataOf("forzar_sync" to true)
val request = OneTimeWorkRequestBuilder()
.setInputData(inputData)
.build()
val workManager = WorkManager.getInstance(context)
workManager.enqueue(request).result.get() // esperar a que se encole
// Ejecutar el worker sincrónicamente en el test
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)!!
testDriver.setAllConstraintsMet(request.id)
val workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state).isEqualTo(WorkInfo.State.SUCCEEDED)
}
}