ensuring work completes even if the app or device restarts, a feature absent in thread-based approaches.
Core Solution
Architecture and Setup
WorkManager operates on a request-based model. You define Worker classes that encapsulate the task logic and WorkRequest objects that specify constraints and scheduling policies. The library manages the execution lifecycle, constraints evaluation, and persistence.
Dependencies:
Ensure you include the Kotlin extensions for coroutine support.
dependencies {
implementation "androidx.work:work-runtime-ktx:2.9.0"
}
Step 1: Define the Worker
Use CoroutineWorker for asynchronous tasks. It integrates seamlessly with Kotlin Coroutines, supports cancellation, and handles lifecycle events efficiently.
class ImageUploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val imageUri = inputData.getString("IMAGE_URI") ?: return Result.failure()
return try {
// Simulate network upload with retry logic
val success = uploadImageToServer(imageUri)
if (success) {
Result.success()
} else {
// Retry with exponential backoff
Result.retry()
}
} catch (e: Exception) {
// Permanent failure due to unexpected error
Result.failure()
}
}
private suspend fun uploadImageToServer(uri: String): Boolean {
// Implementation of upload logic
return true
}
}
Constraints ensure work runs only under optimal conditions, preserving battery and data.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(false)
.build()
Step 3: Enqueue Work Requests
Create and enqueue work requests with appropriate policies. Use setExpedited() for urgent work that should run immediately if resources allow.
val uploadRequest = OneTimeWorkRequestBuilder<ImageUploadWorker>()
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.setInputData(workDataOf("IMAGE_URI" to "content://image/123"))
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED)
.build()
WorkManager.getInstance(context).enqueue(uploadRequest)
Step 4: Chaining and Complex Workflows
WorkManager supports complex workflows via WorkContinuation. This allows sequential and parallel execution with input/output merging.
val compressWork = OneTimeWorkRequestBuilder<ImageCompressWorker>().build()
val uploadWork = OneTimeWorkRequestBuilder<ImageUploadWorker>().build()
val notifyWork = OneTimeWorkRequestBuilder<NotifyUserWorker>().build()
WorkManager.getInstance(context)
.beginWith(compressWork)
.then(uploadWork)
.then(notifyWork)
.enqueue()
Step 5: Observing Results
Observe LiveData to update the UI based on work status.
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadRequest.id)
.observe(viewLifecycleOwner) { workInfo ->
if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
// Update UI
}
}
Architecture Rationale
- CoroutineWorker: Preferred over
Worker for tasks involving I/O or network calls. It provides structured concurrency and automatic cancellation support.
- BackoffPolicy: Essential for transient failures.
EXPONENTIAL backoff prevents server overload and respects network conditions.
- setExpedited: Use for tasks that must run quickly while the app is in the foreground. It falls back to standard scheduling if the expedited quota is exceeded.
- InputMerger: Default
OverwritingInputMerger is sufficient for most cases. Custom mergers are needed for aggregating outputs from parallel workers.
Pitfall Guide
1. Missing BackoffPolicy Configuration
Mistake: Failing to set BackoffPolicy when returning Result.retry().
Impact: The worker will retry immediately, potentially causing a tight loop that drains battery and floods logs.
Best Practice: Always configure setBackoffCriteria with EXPONENTIAL or LINEAR policy.
2. Over-Constraining Tasks
Mistake: Setting constraints that are rarely met, such as requiring charging and unmetered network simultaneously.
Impact: Work never executes. Users report data not syncing.
Best Practice: Review constraints critically. Only add constraints that are strictly necessary for task success. Use setRequiredNetworkType(NetworkType.CONNECTED) as a baseline.
3. Blocking Main Thread in doWork
Mistake: Performing long-running operations on the main thread within a Worker (non-coroutine).
Impact: ANR (Application Not Responding) or process kill.
Best Practice: Use CoroutineWorker for async operations. If using Worker, ensure all work is offloaded to background threads.
4. Ignoring setExpedited for Urgent Work
Mistake: Enqueuing standard work for tasks that must complete before the user leaves the app.
Impact: Work may be delayed until constraints are met or the app enters the background.
Best Practice: Use setExpedited() for urgent tasks. Be aware of the quota limits and define OutOfQuotaPolicy.
5. Memory Leaks via Context References
Mistake: Holding a strong reference to Context or Activity in the Worker class fields.
Impact: Memory leaks if the worker outlives the activity.
Best Practice: Use applicationContext or pass only necessary data via InputData. Avoid storing context references.
6. Not Handling Result.failure() Data
Mistake: Returning Result.failure() without output data when the UI needs error details.
Impact: UI cannot display meaningful error messages.
Best Practice: Use Result.failure(outputData) to pass error codes or messages back to observers.
7. Duplicate Work Enqueueing
Mistake: Calling enqueue() repeatedly without unique work policies.
Impact: Multiple identical tasks run, wasting resources and causing duplicate side effects.
Best Practice: Use WorkManager.enqueueUniqueWork() with ExistingWorkPolicy.REPLACE or KEEP to manage duplicates.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Immediate sync before user action | setExpedited() | Runs immediately if quota allows; falls back gracefully | Higher battery if frequent |
| Periodic log upload | PeriodicWorkRequest | System batches work; respects min interval (15m) | Low |
| Long running task (>10 mins) | ForegroundInfo | Prevents process kill; requires notification | Medium; user notification required |
| Simple network call | CoroutineWorker | Async, cancellable, integrates with coroutines | Low |
| Complex dependency graph | WorkContinuation | Handles sequencing and input merging automatically | Low |
| One-time urgent task | OneTimeWorkRequest + setExpedited | Balances urgency with system constraints | Medium |
Configuration Template
Custom WorkManager initialization for advanced configuration.
class MyWorkManagerInitializer : Initializer<WorkManager> {
override fun create(context: Context): WorkManager {
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setWorkerFactory(MyWorkerFactory())
.build()
WorkManager.initialize(context, config)
return WorkManager.getInstance(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
AndroidManifest.xml:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.MyWorkManagerInitializer"
android:value="androidx.startup" />
</provider>
Quick Start Guide
- Add Dependency: Include
implementation "androidx.work:work-runtime-ktx:2.9.0" in build.gradle.
- Create Worker: Extend
CoroutineWorker and implement doWork() with your task logic.
- Enqueue Request: Build a
OneTimeWorkRequest with constraints and call WorkManager.getInstance(context).enqueue(request).
- Observe Status: Use
getWorkInfoByIdLiveData(request.id) to monitor progress and update the UI.
- Test: Verify execution using Android Studio's Device File Explorer or logs, ensuring work persists across process death.