WorkManager in Android: Background Tasks That Actually Work
When developing Android applications, background tasks are essential for many use cases: syncing data, sending notifications, uploading files, or running periodic checks. However, implementing reliable background task handling can be tricky, especially with Android's varying versions and power management policies.
This is where WorkManager comes in—Google's recommended solution for deferrable background work on Android.
What is WorkManager?
WorkManager is part of Android Jetpack and provides a backwards-compatible way to execute background work that is guaranteed to execute, even if the app is closed or the device restarts. Unlike Handler, Thread, or deprecated IntentService, WorkManager respects system constraints and user preferences.
Key Features:
- Guaranteed execution: Tasks persist across app restarts
- Backwards compatible: Works on API 14+
- Power-aware: Respects Doze mode and battery optimization
- Flexible scheduling: One-time or periodic execution
- Observable: LiveData/Flow for status monitoring
- Chainable: Execute dependent tasks in sequence
OneTimeWorkRequest: Single Tasks
For one-off background tasks that should execute once, use OneTimeWorkRequest:
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import android.content.Context
class UploadDataWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
return try {
// Your background work here
val data = fetchLocalData()
uploadToServer(data)
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
private fun fetchLocalData(): String {
return "sample_data"
}
private fun uploadToServer(data: String) {
// Simulate API call
println("Uploading: $data")
}
}
// Schedule the work
val uploadWork = OneTimeWorkRequestBuilder<UploadDataWorker>()
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"upload_data",
ExistingWorkPolicy.KEEP,
uploadWork
)
Key points:
- Return
Result.success()for successful completion - Return
Result.retry()to retry (with exponential backoff) - Return
Result.failure()to stop and not retry - WorkManager automatically retries with exponential backoff (15 sec, 30 sec, 1 hour, etc.)
PeriodicWorkRequest: Repeating Tasks
For recurring tasks like syncing data or checking for updates:
import androidx.work.PeriodicWorkRequestBuilder
import java.util.concurrent.TimeUnit
class SyncDataWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
return try {
syncWithServer()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
private fun syncWithServer() {
println("Syncing data with server...")
}
}
// Schedule periodic work - minimum interval is 15 minutes
val syncWork = PeriodicWorkRequestBuilder<SyncDataWorker>(
15, TimeUnit.MINUTES
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync_data",
ExistingPeriodicWorkPolicy.KEEP,
syncWork
)
Important: The minimum interval for periodic work is 15 minutes. Shorter intervals will be adjusted by WorkManager.
Constraints: Conditional Execution
Control when tasks should run with constraints:
import androidx.work.Constraints
import androidx.work.NetworkType
// Create constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // Only run when connected
.setRequiresCharging(false) // Works on battery
.setRequiresBatteryNotLow(true) // Don't run if battery low
.setRequiresStorageNotLow(true) // Ensure storage space
.setRequiresDeviceIdle(false) // Can run even when device is in use
.build()
// Apply constraints to work
val uploadWork = OneTimeWorkRequestBuilder<UploadDataWorker>()
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueue(uploadWork)
Common constraints:
-
setRequiredNetworkType(): CONNECTED, METERED, UNMETERED, NOT_REQUIRED -
setRequiresCharging(): Run only while charging -
setRequiresBatteryNotLow(): Skip if battery is low -
setRequiresStorageNotLow(): Ensure sufficient storage -
setRequiresDeviceIdle(): Run only when Doze is inactive
Chaining: Dependent Work
Execute tasks in sequence, passing data between them:
import androidx.work.OneTimeWorkRequest
class WorkerA(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val output = Data.Builder()
.putString("key", "result_from_a")
.build()
return Result.success(output)
}
}
class WorkerB(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val input = inputData.getString("key") ?: ""
println("Received from A: $input")
return Result.success()
}
}
// Chain them
val workA = OneTimeWorkRequestBuilder<WorkerA>().build()
val workB = OneTimeWorkRequestBuilder<WorkerB>().build()
WorkManager.getInstance(context)
.beginWith(workA)
.then(workB)
.enqueue()
You can also use beginUniqueWork() and enqueueUniqueWork() to prevent duplicate chains:
WorkManager.getInstance(context)
.beginUniqueWork("data_sync", ExistingWorkPolicy.KEEP, workA)
.then(workB)
.enqueue()
Observing Work Status
Monitor your work's progress and status:
// Observe single work
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadWork.id)
.observe(lifecycleOwner) { workInfo ->
when {
workInfo?.state == WorkInfo.State.SUCCEEDED -> {
println("Work succeeded!")
}
workInfo?.state == WorkInfo.State.FAILED -> {
println("Work failed!")
}
workInfo?.state == WorkInfo.State.RUNNING -> {
println("Work in progress...")
}
}
}
// Or observe by tag
WorkManager.getInstance(context)
.getWorkInfosByTagLiveData("my_tag")
.observe(lifecycleOwner) { workInfoList ->
workInfoList?.forEach { workInfo ->
println("Work ${workInfo.id}: ${workInfo.state}")
}
}
WorkInfo states:
-
ENQUEUED: Waiting to execute -
RUNNING: Currently executing -
SUCCEEDED: Completed successfully -
FAILED: Failed and won't retry -
BLOCKED: Dependent work failed -
CANCELLED: Manually cancelled
Best Practices
- Keep workers short-lived: Complete work in seconds, not minutes
- Handle exceptions gracefully: Always return appropriate Result
- Use constraints wisely: Reduce battery drain by being smart about conditions
- Monitor with logging: Observe work status in production
- Test on real devices: Emulator behavior differs from actual devices
- Use unique work names: Prevent duplicate task scheduling
-
Cancel work when needed: Call
WorkManager.getInstance(context).cancelUniqueWork()
Conclusion
WorkManager is the backbone of reliable background task handling on Android. By understanding OneTimeWorkRequest, PeriodicWorkRequest, constraints, and chaining, you can build robust apps that perform work reliably, even in challenging conditions.
My 8 Android templates include background task patterns. https://myougatheax.gumroad.com
Top comments (0)