¿Qué es WorkManager?
WorkManager es la API recomendada de Jetpack para tareas en background que deben ejecutarse de forma garantizada, incluso si el usuario cierra la app o el sistema la mata.
Internamente usa JobScheduler (API 23+) y maneja Doze, App Standby y restricciones de red automáticamente. Vos definís qué hacer y las condiciones — WorkManager decide cuándo hacerlo.
Cuándo usar WorkManager:
- Subir fotos a un servidor en background
- Sincronizar la base de datos local con el servidor
- Comprimir archivos grandes
- Procesar logs y analytics antes de enviarlos
- Cualquier trabajo diferible que debe completarse eventualmente
WorkManager no es para trabajo inmediatoWorkManager garantiza que el trabajo se ejecutará, pero no cuándo exactamente. Si necesitás respuesta inmediata mientras el usuario espera, usá coroutines en el viewModelScope. WorkManager es para trabajo que puede esperar minutos u horas.
Setup
// build.gradle (app)
dependencies {
val work_version = "2.9.0"
implementation("androidx.work:work-runtime-ktx:$work_version")
// Con Hilt
implementation("androidx.hilt:hilt-work:1.2.0")
ksp("androidx.hilt:hilt-compiler:1.2.0")
}
Crear un Worker
Un Worker es la unidad de trabajo. Extiende Worker o CoroutineWorker e implementa doWork():
// Worker básico — corre en un thread de background
class SyncWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
return try {
// Trabajo real — corre en un background thread automáticamente
val response = apiService.sync()
database.save(response)
Result.success()
} catch (e: Exception) {
// Si falla, WorkManager puede reintentar (con backoff)
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
CoroutineWorker — la forma moderna
CoroutineWorker tiene soporte nativo para suspend functions. doWork() es una suspend function que corre en Dispatchers.Default:
class SyncWorker(
context: Context,
params: WorkerParameters,
private val repository: ProductoRepository // inyectado con Hilt
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// suspend functions directamente — sin withContext necesario
val productos = repository.fetchFromApi()
repository.saveAll(productos)
// Pasar datos al siguiente Worker en la cadena
val outputData = workDataOf("count" to productos.size)
Result.success(outputData)
} catch (e: IOException) {
// Reintentar si hay error de red
if (runAttemptCount < 3) Result.retry()
else Result.failure(workDataOf("error" to e.message))
} catch (e: Exception) {
Result.failure(workDataOf("error" to e.message))
}
}
}
// Con Hilt — Factory para inyección de dependencias
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repository: ProductoRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result { /* ... */ return Result.success() }
}
Constraints — condiciones para ejecutar
val constraints = Constraints.Builder()
// Solo ejecutar si hay red
.setRequiredNetworkType(NetworkType.CONNECTED)
// Solo con red NO medida (WiFi, no datos móviles)
.setRequiredNetworkType(NetworkType.UNMETERED)
// Solo si la batería no está baja
.setRequiresBatteryNotLow(true)
// Solo si está cargando
.setRequiresCharging(true)
// Solo si hay espacio en disco
.setRequiresStorageNotLow(true)
// Solo si el dispositivo está idle (sin uso)
.setRequiresDeviceIdle(true) // API 23+
.build()
// Ejemplo real: sincronización de backup
// Solo cuando hay WiFi, cargando y el dispositivo está inactivo
val backupConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.setRequiresDeviceIdle(true)
.build()
Input y output data
// Pasar datos de entrada al Worker
val inputData = workDataOf(
"producto_id" to 42,
"forzar_sync" to true,
"categoria" to "Electronics"
)
// Leerlos dentro del Worker:
override suspend fun doWork(): Result {
val productoId = inputData.getInt("producto_id", -1)
val forzar = inputData.getBoolean("forzar_sync", false)
val categoria = inputData.getString("categoria")
// ...
return Result.success()
}
// Limitación importante: máximo 10KB por Data object
// Para más datos, guardá en Room/DataStore y pasá solo el ID
Encolar trabajo
// OneTimeWorkRequest — se ejecuta una vez
val syncRequest = OneTimeWorkRequestBuilder()
.setConstraints(constraints)
.setInputData(workDataOf("categoria" to "Electronics"))
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, // espera 10s, luego 20s, 40s...
10, TimeUnit.SECONDS
)
.addTag("sync_productos") // para cancelar o consultar por tag
.build()
// Encolar en WorkManager
WorkManager.getInstance(context).enqueue(syncRequest)
// enqueueUniqueWork — evitar duplicados
WorkManager.getInstance(context).enqueueUniqueWork(
"sync_productos_unique", // nombre único
ExistingWorkPolicy.KEEP, // si ya existe, no reemplazar
// ExistingWorkPolicy.REPLACE // cancelar el existente y empezar de nuevo
// ExistingWorkPolicy.APPEND // encolar detrás del existente
syncRequest
)
La garantía de ejecución
WorkManager persiste el trabajo en una base de datos Room interna. Esto garantiza que:
- Si el proceso muere durante la ejecución, el trabajo se reinicia automáticamente
- Si el dispositivo se reinicia, el trabajo pendiente se reanuda
- Si las constraints no se cumplen (sin red), el trabajo espera hasta que se cumplan
- El trabajo se ejecuta en la próxima ventana de mantenimiento de Doze
Inspeccionar el estado de WorkManagerEn Android Studio → App Inspection → WorkManager puedes ver todos los workers encolados, su estado (ENQUEUED, RUNNING, SUCCEEDED, FAILED) y sus datos de entrada/salida. Es invaluable para debugging.