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)
    }
}