I04 — WorkManager sync
Schedule deferrable work with constraints, survive reboots and process death, and read the scheduler's state from the shell. WorkManager is the supported answer to background work, and its guarantees are narrower than most people assume.
WorkManager is the library everyone is told to use and few can describe precisely. This project makes its guarantees concrete: constraints, retries, unique work, and the difference between "enqueued" and "run".
What you are building#
A sync worker that simulates a network call: it waits, fails intermittently, and retries with backoff. The app shows the work's state and lets you constrain it to "network connected" and "charging", so you can watch the scheduler hold the work until the constraints are met.
Steps#
- Add WorkManager
Add the WorkManager runtime and the testing artifact if you want to test without a device. You do not need a separate scheduler library; WorkManager uses JobScheduler internally on modern platforms.1
You should seeThe dependency resolves. - Write the worker
Extend
CoroutineWorkerand implementdoWork. ReturnResult.success(),Result.retry()orResult.failure()based on the simulated outcome.You should seeThe worker runs and returns a result. - Add constraints
Build a
Constraintsobject with a network requirement and a charging requirement, and enqueue the work with them. Toggle airplane mode or unplug the charger and observe the state.You should seeThe work waits when the constraint is unmet. - Add backoff
Return
Result.retry()on failure and set a backoff policy and delay in the request. The retry is scheduled, not immediate, and the delay grows.You should seeA retry is delayed. - Make it unique
Use
enqueueUniqueWorkwithExistingWorkPolicy.KEEPorREPLACE, and observe the difference. Unique work is how you avoid a queue of duplicate syncs.You should seeA second enqueue does not create a second chain. - Observe the state
Collect the work's
WorkInfostate and render it. The states are the vocabulary of the system: ENQUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED, BLOCKED.You should seeStates change from ENQUEUED to RUNNING to SUCCEEDED. - Kill the app
Force-stop the app and observe that the work is cancelled (force-stop cancels scheduled work), then relaunch and see that the persisted work resumes. This is the difference between a process and a scheduled job.2
You should seeThe work still runs. - Read the scheduler
Dump the JobScheduler state and find your package. The work is a job with constraints, and the dump shows whether it is ready, waiting or running.
You should seeA job entry for your work.
The worker and the request:
class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
return if (sync()) Result.success() else Result.retry()
}
}
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()adb shell dumpsys jobscheduler | grep -i -A 5 com.example.sync
adb shell cmd jobscheduler run com.example.sync 1
adb shell dumpsys deviceidle force-idleExample output
Illustrative output — job ids and package names differ.Verification#
The work should run when the constraints are met and wait when they are not. A useful test: enqueue with a charging constraint, unplug the device, and confirm the state stays ENQUEUED. Then plug it in and watch it run.
What you learned#
- WorkManager guarantees eventual execution, not immediate execution. The constraints and the system's batching decide when.
- Unique work is a queue policy, not a lock. KEEP, REPLACE and APPEND have different semantics and different failure modes.
- Force-stop cancels scheduled work, which is why a force-stopped app does not sync.
- The scheduler is visible.
dumpsys jobscheduleris the ground truth for what the system thinks is scheduled.
Read Choosing the right background API for the decision tree, and Doze and App Standby for when work is deferred.
Extend it#
- Chain two workers with
beginWith().then()and observe the ordering and the failure propagation. - Add a periodic worker with a 15-minute minimum interval and watch it run across a device reboot.
- Add a foreground worker for a long task and compare the notification and the timeout rules with a foreground service.
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsWorkmanagerdeveloper.android.comaccessed 2026-10-27↩
- docsBackground tasks overview | Background workdeveloper.android.comaccessed 2026-10-27↩
- docsOptimize for Doze and App Standbydeveloper.android.comaccessed 2026-10-27↩
- docsCustom WorkManager configurationdeveloper.android.comaccessed 2026-10-27↩
- docsStandby bucketsdeveloper.android.comaccessed 2026-10-27↩
- docsForeground services overview | Background workdeveloper.android.comaccessed 2026-10-27↩
- docsOptimize power usedeveloper.android.comaccessed 2026-10-27↩