On this page
Android aggressively limits background execution to save battery. WorkManager works within those limits: it persists scheduled work in a database, respects constraints like network and charging, retries with backoff, and survives app restarts and reboots.
When to use WorkManager
Setup and a CoroutineWorker
dependencies {
implementation("androidx.work:work-runtime-ktx:2.10.0") // check for the latest stable version
}class SyncWorker(
appContext: Context,
params: WorkerParameters,
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
val since = inputData.getLong(KEY_SINCE, 0L)
return try {
val count = syncRepository().syncChangesSince(since) // suspend, main-safe
Result.success(workDataOf(KEY_COUNT to count))
} catch (e: IOException) {
if (runAttemptCount < 5) Result.retry() else Result.failure()
}
}
companion object {
const val KEY_SINCE = "since"
const val KEY_COUNT = "count"
}
}Return success, retry (uses backoff) or failure. Treat work as idempotent: it may run more than once.
Enqueue with constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
// One-time, unique: avoid duplicate syncs if the user taps twice
val oneTime = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setInputData(workDataOf(SyncWorker.KEY_SINCE to lastSync))
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, oneTime)
// Periodic: minimum interval is 15 minutes
val periodic = PeriodicWorkRequestBuilder<SyncWorker>(12, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork("periodic-sync", ExistingPeriodicWorkPolicy.KEEP, periodic)WorkManager runs work when constraints are met and the system allows. Doze and App Standby buckets can delay it. Don't use it when work must happen at a precise time.
Chaining and observing
WorkManager.getInstance(context)
.beginUniqueWork("upload", ExistingWorkPolicy.REPLACE, compressRequest)
.then(uploadRequest) // receives compress output as input
.then(cleanupRequest)
.enqueue()
// Observe progress/state as Flow (e.g. in a ViewModel)
val uploadState: Flow<WorkInfo.State?> =
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkFlow("upload")
.map { infos -> infos.lastOrNull()?.state }Expedited and long-running work
- Expedited work (
setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)): for short, important tasks the user started, like sending a message. Subject to quotas. - Long-running work: call
setForeground(...)from the worker to run as a foreground service with a notification, declaring the right foreground service type. See Foreground Services.
Dependency injection with Hilt
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val repository: SyncRepository,
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result = /* ... */ Result.success()
}
// Application implements Configuration.Provider with HiltWorkerFactory,
// and the default WorkManager initializer is removed in the manifest.Testing
@Test
fun syncWorker_succeeds() = runTest {
val worker = TestListenableWorkerBuilder<SyncWorker>(context)
.setInputData(workDataOf(SyncWorker.KEY_SINCE to 0L))
.build()
val result = worker.doWork()
assertTrue(result is ListenableWorker.Result.Success)
}Use cases
Offline-first sync
Push queued local changes when the network returns. See Offline-First Apps.
Media upload
Compress, upload and clean up as a chain, with progress shown in the UI.
Maintenance
Periodic cache cleanup or database pruning while charging.
Troubleshooting
- Work never runs: check constraints, inspect with Background Task Inspector in Android Studio, and check if the app is force-stopped (force-stop cancels scheduled work until the app is opened again).
- Duplicate work: use unique work with the right existing-work policy.
- Hilt worker crashes on creation: the custom WorkManager configuration or initializer removal is missing.
Frequently asked questions
Does WorkManager survive reboots?
Yes. Work is persisted and rescheduled after reboot.
Can periodic work run every minute?
No. The minimum interval is 15 minutes. For real-time updates use push messages (FCM) or a foreground service when appropriate.
Conclusion
Use WorkManager for persistent, deferrable work: write idempotent CoroutineWorkers, add constraints, use unique work to avoid duplicates, chain steps, observe state as Flow, and test with TestListenableWorkerBuilder.
Next step: handle ongoing, user-visible tasks with Foreground Services in Android.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Walzi4K wallpapers for lock and home screenGet it โธ
Ready to build real Android applications?
Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.