What it is
WorkManager schedules deferrable, guaranteed background work on Android, surviving process death and reboots while respecting battery restrictions.
Define a Worker, wrap it in a WorkRequest with constraints, and enqueue it. WorkManager persists the request and runs it when the constraints are satisfied.
Installation
implementation("androidx.work:work-runtime-ktx:2.9.1")Getting started
The smallest useful thing you can do with it, and what each part means.
class SyncWorker(ctx: Context, params: WorkerParameters) :
CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result = try {
val since = inputData.getLong("since", 0)
repository.sync(since)
Result.success()
} catch (e: IOException) {
// Retry with the configured backoff — transient failure.
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.setInputData(workDataOf("since" to lastSync))
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, request)Advanced usage
Where the library earns its place over a simpler alternative.
WorkManager.getInstance(context)
.beginWith(listOf(compressRequest, uploadThumbRequest)) // parallel
.then(uploadRequest) // then this
.then(notifyRequest)
.enqueue()
// Report progress from the worker.
setProgress(workDataOf("progress" to percent))
// Observe it.
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkFlow("sync")
.collect { infos ->
val info = infos.firstOrNull() ?: return@collect
when (info.state) {
WorkInfo.State.RUNNING -> showProgress(info.progress.getInt("progress", 0))
WorkInfo.State.SUCCEEDED -> showDone()
WorkInfo.State.FAILED -> showError()
else -> Unit
}
}Errors and fixes
The failures you are most likely to hit, and what actually resolves them.
- Work never runs
- Constraints are unmet, or the device is in Doze. Check with getWorkInfos, and confirm the constraints are actually satisfiable.
- IllegalStateException: Data cannot occupy more than 10240 bytes
- Too much data passed in or out. Store the payload and pass a reference.
Best practices
- Use enqueueUniqueWork to prevent duplicate jobs accumulating.
- Pass identifiers in Data, never payloads — there is a ~10 KB limit.
- Return Result.retry() only for transient failures, and cap attempts with runAttemptCount.
- Use WorkManager for deferrable work; it is the wrong tool for anything needing to run immediately.
Background
Why it exists, and what it was reacting to.
Android's background execution limits made older approaches unreliable. WorkManager is the single recommended API: it picks the right underlying mechanism per OS version and persists work so it eventually runs.
