Implementing WorkManager for Background Tasks in Android Applications
WorkManager is the standard API for guaranteed background tasks: file uploads, data synchronization, analytics reporting. It's not a replacement for Coroutines for one-off operations—WorkManager is for tasks that must complete even if the application is killed or the device reboots.
Core Concepts
Worker / CoroutineWorker is the unit of work. WorkRequest is a task with configuration. WorkManager is the scheduler, implemented on top of JobScheduler (API 23+), AlarmManager (older versions), and Firebase JobDispatcher (if available). Selection is automatic.
CoroutineWorker is the preferred option for Kotlin:
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val userId = inputData.getString(KEY_USER_ID) ?: return Result.failure()
syncRepository.syncUser(userId)
Result.success()
} catch (e: IOException) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
companion object {
const val KEY_USER_ID = "user_id"
}
}
runAttemptCount is the retry counter. Result.retry() combined with BackoffPolicy determines when WorkManager will retry the task.
Task Configuration
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(workDataOf(SyncWorker.KEY_USER_ID to userId))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.MINUTES)
.addTag("sync_task")
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"user_sync_$userId",
ExistingWorkPolicy.KEEP,
syncRequest
)
enqueueUniqueWork with ExistingWorkPolicy.KEEP prevents duplicate tasks if one with the same name is already active. Without this, a user tapping "Sync" twice launches two parallel workers.
Periodic Tasks
val periodicRequest = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()
)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"hourly_sync",
ExistingPeriodicWorkPolicy.UPDATE,
periodicRequest
)
The minimum interval for PeriodicWorkRequest is 15 minutes. Shorter intervals are ignored and the system runs by its own schedule. ExistingPeriodicWorkPolicy.UPDATE (introduced in WorkManager 2.8.0) updates the settings of an existing periodic task without canceling it.
Task Chains
WorkManager.getInstance(context)
.beginUniqueWork("upload_chain", ExistingWorkPolicy.REPLACE,
OneTimeWorkRequestBuilder<CompressWorker>().build()
)
.then(OneTimeWorkRequestBuilder<UploadWorker>().build())
.then(OneTimeWorkRequestBuilder<NotifyWorker>().build())
.enqueue()
If CompressWorker returns Result.failure(), the chain stops and UploadWorker doesn't run. Result.success(outputData) passes data to the next worker via inputMerger.
Observing Status
WorkManager.getInstance(context)
.getWorkInfosByTagLiveData("sync_task")
.observe(viewLifecycleOwner) { workInfos ->
workInfos?.forEach { info ->
when (info.state) {
WorkInfo.State.RUNNING -> showProgress()
WorkInfo.State.SUCCEEDED -> showSuccess()
WorkInfo.State.FAILED -> showError()
else -> Unit
}
}
}
Common Project Issues
Tasks don't run on Chinese devices (Xiaomi, Huawei). Aggressive battery savers kill background processes. WorkManager uses JobScheduler, which these manufacturers override. The only reliable workaround is setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) for time-critical tasks and documenting limitations in README. Guaranteeing precise background task execution time on these devices is impossible.
Context leaks. Worker receives applicationContext, but developers sometimes capture Activity in lambdas. Workers live longer than Activities—crashes occur when accessing destroyed contexts.
Too much data in inputData. The limit is 10 KB. Pass IDs, not serialized objects. Read data from Room or SharedPreferences inside the worker.
Integration with Hilt
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val syncRepository: SyncRepository
) : CoroutineWorker(context, params) { ... }
// In Application
@HiltAndroidApp
class App : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration() =
Configuration.Builder().setWorkerFactory(workerFactory).build()
}
Without a custom Configuration.Provider, Hilt cannot inject dependencies into Worker—WorkManager creates workers using its default factory.
Implementing WorkManager with a basic task set takes 2–4 days. Complex chains with error handling, synchronization, and tests take up to 2 weeks. Cost is calculated individually.







