Section 1 · WorkManager
Guaranteed, deferrable background work that survives process death and respects system constraints
What WorkManager Is (and Is Not)
WorkManager is Jetpack’s recommended solution for deferrable, guaranteed background work — work that must eventually run even if the app is killed or the device restarts. It is not a general coroutine scheduler or a replacement for threads; it is a persistent job scheduler backed by the platform.
Google Android Developers — WorkManager overview
Under the hood, WorkManager chooses the best available platform API automatically:
| API level | Backing implementation |
|---|---|
| API 23+ | JobScheduler |
| API < 23 with Google Play Services | GCMNetworkManager |
| API < 23 without Play Services | AlarmManager + BroadcastReceiver |
Your code never changes — WorkManager abstracts all of this.
When to use WorkManager
| Scenario | Right tool |
|---|---|
| Sync data to server when network is available | WorkManager |
| Upload logs or analytics on charging + unmetered network | WorkManager |
| Compress images on idle + sufficient storage | WorkManager |
| Re-schedule background work after device reboot | WorkManager |
| Play music while the screen is off | Foreground Service |
| React to a push notification right now | FCM + immediate coroutine |
| Execute code every 100ms | Timer / coroutine, not WorkManager |
WorkManager is for deferrable work. If the task must run within a few seconds of the user action, launch a coroutine instead. WorkManager is free to run the work up to 15 minutes late to satisfy constraints and battery optimisation.
Dependency
1
2
3
4
// build.gradle.kts (app)
dependencies {
implementation("androidx.work:work-runtime-ktx:2.9.1")
}
The Worker
All work is defined by extending CoroutineWorker (preferred for coroutine-based code) or ListenableWorker.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val count = repository.syncWithServer()
Result.success(
workDataOf("synced_count" to count) // optional output data
)
} catch (e: IOException) {
if (runAttemptCount < 3) Result.retry() // ask WorkManager to retry
else Result.failure(workDataOf("error" to e.message))
}
}
}
doWork() runs on a background dispatcher automatically. Return one of:
| Result | Meaning |
|---|---|
Result.success() |
Work completed; do not re-run |
Result.failure() |
Work failed permanently |
Result.retry() |
Ask WorkManager to retry according to backoff policy |
OneTimeWorkRequest
For work that should run once.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.setInputData(workDataOf("user_id" to userId))
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.addTag("sync")
.build()
WorkManager.getInstance(context).enqueue(syncRequest)
PeriodicWorkRequest
For work that repeats on a schedule. Minimum interval is 15 minutes (platform constraint from JobScheduler).
1
2
3
4
5
6
7
8
9
10
11
12
13
val periodicSync = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS,
flexTimeInterval = 15,
flexTimeIntervalUnit = TimeUnit.MINUTES // run in last 15 min of each hour
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
)
.build()
The flexTimeInterval defines a window at the end of each period during which WorkManager will try to run the work.
Constraints
1
2
3
4
5
6
7
8
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // any network
.setRequiredNetworkType(NetworkType.UNMETERED) // Wi-Fi only
.setRequiresCharging(true) // plugged in
.setRequiresBatteryNotLow(true) // battery above threshold
.setRequiresDeviceIdle(true) // Doze-idle (API 23+)
.setRequiresStorageNotLow(true) // sufficient free space
.build()
| Constraint | Android docs threshold |
|---|---|
| Battery not low | Below ~15% triggers the condition |
| Storage not low | Below ~1 GB (device-specific) triggers the condition |
| Device idle | Device in Doze mode with screen off and stationary |
Unique Work
enqueueUniqueWork / enqueueUniquePeriodicWork ensures only one instance of a named job exists at a time.
1
2
3
4
5
6
7
8
9
10
11
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"hourly-sync", // unique name
ExistingPeriodicWorkPolicy.KEEP, // keep the existing one if running
periodicSync
)
WorkManager.getInstance(context).enqueueUniqueWork(
"one-time-upload",
ExistingWorkPolicy.REPLACE, // cancel existing, enqueue new
uploadRequest
)
| Policy | Behaviour |
|---|---|
KEEP |
If a job with the same name is already enqueued/running, ignore the new request |
REPLACE |
Cancel the existing job and enqueue the new one |
APPEND |
New work runs after the existing chain completes |
APPEND_OR_REPLACE |
Appends unless existing work is cancelled/failed, then replaces |
Work Chaining
WorkManager supports sequential and parallel chains of workers.
1
2
3
4
5
6
7
8
9
10
11
12
// Sequential chain
WorkManager.getInstance(context)
.beginWith(cleanupRequest)
.then(uploadRequest)
.then(notifyRequest)
.enqueue()
// Parallel then merge
val parallel = WorkManager.getInstance(context)
.beginWith(listOf(fetchImagesRequest, fetchMetadataRequest)) // run in parallel
.then(mergeRequest) // runs after both finish
.enqueue()
Output data from one worker is passed as input data to the next via Result.success(workDataOf(...)).
Expedited Work
For urgent tasks that need to run as soon as possible while the app may be in the background. Requires the worker to call getForegroundInfo().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UrgentSyncWorker(ctx: Context, params: WorkerParameters)
: CoroutineWorker(ctx, params) {
override suspend fun getForegroundInfo(): ForegroundInfo =
ForegroundInfo(
NOTIFICATION_ID,
buildNotification(applicationContext)
)
override suspend fun doWork(): Result {
// urgent work here
return Result.success()
}
}
val expedited = OneTimeWorkRequestBuilder<UrgentSyncWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
Google — WorkManager expedited work guide
Observing Work
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Observe by ID
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(syncRequest.id)
.observe(lifecycleOwner) { info ->
when (info?.state) {
WorkInfo.State.ENQUEUED -> showStatus("Queued")
WorkInfo.State.RUNNING -> showStatus("Running")
WorkInfo.State.SUCCEEDED -> {
val count = info.outputData.getInt("synced_count", 0)
showStatus("Synced $count items")
}
WorkInfo.State.FAILED -> showStatus("Failed")
WorkInfo.State.CANCELLED -> showStatus("Cancelled")
else -> Unit
}
}
// Observe by tag (Flow-based)
WorkManager.getInstance(context)
.getWorkInfosByTagFlow("sync")
.collect { infos -> /* ... */ }
Input and Output Data
Data is a lightweight key-value store (max 10 KB) for passing parameters in and results out.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Input — pass to worker at enqueue time
val input = workDataOf(
"image_url" to "https://example.com/photo.jpg",
"compress" to true,
"quality" to 80
)
// Inside doWork()
val url = inputData.getString("image_url") ?: return Result.failure()
val compress = inputData.getBoolean("compress", false)
val quality = inputData.getInt("quality", 100)
// Output — returned from doWork()
Result.success(workDataOf("saved_path" to "/data/user/0/.../photo.jpg"))
Retry and Backoff
1
2
3
4
5
6
7
OneTimeWorkRequestBuilder<SyncWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, // or LINEAR
WorkRequest.MIN_BACKOFF_MILLIS, // 10 000 ms minimum
TimeUnit.MILLISECONDS
)
.build()
BackoffPolicy.EXPONENTIAL: delay doubles each retry (10 s → 20 s → 40 s …, up to 5 hours)BackoffPolicy.LINEAR: delay grows linearly (10 s, 20 s, 30 s …)Result.retry()indoWork()triggers the next attempt;runAttemptCounttracks how many times it has been tried
Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Use the synchronous executor in tests
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
val workManager = WorkManager.getInstance(context)
val request = OneTimeWorkRequestBuilder<SyncWorker>().build()
workManager.enqueue(request).result.get()
val info = workManager.getWorkInfoById(request.id).get()
assertThat(info.state).isEqualTo(WorkInfo.State.SUCCEEDED)
← Previous: Services · ↑ Chapter Index · Next: BroadcastReceiver →