Background Work on Android: The Problem WorkManager Solves
Defining Workers: OneTime and Periodic
// Coroutine-based worker for syncing articles
class ArticleSyncWorker(
appContext: Context,
params: WorkerParameters,
private val articleRepo: ArticleRepository // Injected via Hilt
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
// Read input data
val forceRefresh = inputData.getBoolean("force_refresh", false)
return try {
val syncCount = articleRepo.syncWithRemote(forceRefresh)
// Pass result data to the next worker in the chain
val output = workDataOf(
"sync_count" to syncCount,
"synced_at" to System.currentTimeMillis()
)
Result.success(output)
} catch (e: HttpException) {
if (e.code() in 500..599) {
// Server error: retry with exponential backoff
Result.retry()
} else {
// Client error (4xx): don't retry
Result.failure(workDataOf("error" to e.message))
}
} catch (e: IOException) {
// Network error: retry
Result.retry()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
NOTIFICATION_ID,
createNotification("Syncing articles...")
)
}
}
// Hilt integration: custom WorkerFactory
@HiltWorker
class ArticleSyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val articleRepo: ArticleRepository
) : CoroutineWorker(appContext, params)Constraints: Running Work at the Right Time
// Constraints: only sync on Wi-Fi while charging with sufficient battery
val syncConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // Wi-Fi only
.setRequiresCharging(true) // Plugged in
.setRequiresBatteryNotLow(true) // Battery > 15%
.setRequiresStorageNotLow(true) // Storage available
.build()
// One-time sync with constraints and backoff
val syncRequest = OneTimeWorkRequestBuilder<ArticleSyncWorker>()
.setConstraints(syncConstraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30, TimeUnit.SECONDS // 30s, 60s, 120s, 240s...
)
.setInputData(workDataOf("force_refresh" to true))
.addTag("article_sync")
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"article_sync", // Unique name
ExistingWorkPolicy.KEEP, // Don't restart if already running
syncRequest
)
// Periodic sync: every 6 hours on any network
val periodicSync = PeriodicWorkRequestBuilder<ArticleSyncWorker>(
repeatInterval = 6, TimeUnit.HOURS,
flexInterval = 30, TimeUnit.MINUTES // Can run 30min early
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
).addTag("periodic_sync")
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"periodic_article_sync",
ExistingPeriodicWorkPolicy.UPDATE, // Update existing schedule
periodicSync
)Chaining Work: Complex Pipelines
// Chain: Download images → Compress → Upload → Notify
val downloadWork = OneTimeWorkRequestBuilder<DownloadImagesWorker>()
.setConstraints(networkConstraints)
.addTag("image_pipeline")
.build()
val compressWork = OneTimeWorkRequestBuilder<CompressImagesWorker>()
.addTag("image_pipeline")
.build()
val uploadWork = OneTimeWorkRequestBuilder<UploadImagesWorker>()
.setConstraints(networkConstraints)
.addTag("image_pipeline")
.build()
val notifyWork = OneTimeWorkRequestBuilder<NotifyCompletionWorker>()
.build()
WorkManager.getInstance(context)
.beginWith(downloadWork) // Step 1
.then(compressWork) // Step 2 (gets output from step 1)
.then(uploadWork) // Step 3
.then(notifyWork) // Step 4
.enqueue()
// Parallel work: fetch from multiple APIs, then merge
val fetchUsers = OneTimeWorkRequestBuilder<FetchUsersWorker>().build()
val fetchPosts = OneTimeWorkRequestBuilder<FetchPostsWorker>().build()
val fetchComments = OneTimeWorkRequestBuilder<FetchCommentsWorker>().build()
val mergeResults = OneTimeWorkRequestBuilder<MergeDataWorker>().build()
WorkManager.getInstance(context)
.beginWith(listOf(fetchUsers, fetchPosts, fetchComments)) // Parallel
.then(mergeResults) // Merge
.enqueue()Observing Work Progress in Compose
@HiltViewModel
class SyncViewModel @Inject constructor(
private val workManager: WorkManager
) : ViewModel() {
// Observe all sync workers by tag
val syncState: Flow<SyncUiState> = workManager
.getWorkInfosByTagFlow("article_sync")
.map { workInfos ->
val latest = workInfos.lastOrNull()
when (latest?.state) {
WorkInfo.State.RUNNING -> SyncUiState.Syncing(
progress = latest.progress.getInt("progress", 0)
)
WorkInfo.State.SUCCEEDED -> SyncUiState.Success(
count = latest.outputData.getInt("sync_count", 0)
)
WorkInfo.State.FAILED -> SyncUiState.Error(
message = latest.outputData.getString("error") ?: "Sync failed"
)
WorkInfo.State.ENQUEUED -> SyncUiState.Waiting
else -> SyncUiState.Idle
}
}
fun startSync() {
val request = OneTimeWorkRequestBuilder<ArticleSyncWorker>()
.addTag("article_sync")
.build()
workManager.enqueueUniqueWork(
"manual_sync",
ExistingWorkPolicy.KEEP,
request
)
}
fun cancelSync() {
workManager.cancelUniqueWork("manual_sync")
}
}
sealed interface SyncUiState {
data object Idle : SyncUiState
data object Waiting : SyncUiState
data class Syncing(val progress: Int) : SyncUiState
data class Success(val count: Int) : SyncUiState
data class Error(val message: String) : SyncUiState
}
@Composable
fun SyncButton(viewModel: SyncViewModel = hiltViewModel()) {
val state by viewModel.syncState
.collectAsStateWithLifecycle(SyncUiState.Idle)
when (state) {
is SyncUiState.Syncing -> {
LinearProgressIndicator(
progress = { (state as SyncUiState.Syncing).progress / 100f }
)
}
is SyncUiState.Error -> {
Button(onClick = { viewModel.startSync() }) {
Text("Retry Sync")
}
}
else -> {
Button(onClick = { viewModel.startSync() }) {
Text("Sync Now")
}
}
}
}Key Takeaways
- 1WorkManager guarantees execution even across app restarts and device reboots -- it is the only correct choice for deferrable background work on modern Android.
- 2Use CoroutineWorker for suspend-function-based work, and return Result.retry() with exponential backoff for transient failures.
- 3Constraints (network, charging, battery, storage) let you declaratively schedule work for optimal conditions without manual checks.
- 4Work chains enable sequential and parallel pipelines where output data flows between workers automatically.
- 5Observe WorkInfo flows in Compose to build reactive progress UIs that reflect background work state in real time.
- 6PeriodicWorkRequests have a minimum interval of 15 minutes and support flex windows for battery-efficient scheduling.
Frequently Asked
WorkManager vs JobScheduler vs AlarmManager?
Use WorkManager for most background work. It uses JobScheduler on API 23+ and falls back to AlarmManager + BroadcastReceiver on older devices. Only use JobScheduler directly for system-level integration.
Why isn't my work running immediately?
WorkManager is not for immediate or foreground work. It's optimized for deferrable, guaranteed execution. Use foreground service for immediate work. WorkManager may delay based on constraints and system state.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.