You need your app to sync data in the background. Maybe it's uploading user activity, refreshing a feed, or sending a queued message that failed earlier. The old answer was SyncAdapter — a heavyweight framework that required a fake account, a stub ContentProvider, and a lot of boilerplate just to run a background task. Nobody misses it.
The modern answer is WorkManager. It handles scheduling, retries, constraints (like "only run on Wi-Fi"), and survives app restarts and device reboots. It's the right tool for any background work that must complete eventually — even if the user closes the app or the device restarts. This guide covers everything from a basic worker to chained tasks and observing results in your UI.
When to Use WorkManager
WorkManager is not for everything. Here's how to decide:
| Use Case | Right Tool |
|---|---|
| Sync data that must complete even if app is closed | ✅ WorkManager |
| Upload logs, analytics, crash reports in background | ✅ WorkManager |
| Periodic data refresh (every 15+ minutes) | ✅ WorkManager PeriodicWorkRequest |
| Fetch data while user is actively using the app | ✅ Retrofit + Coroutines |
| Real-time updates (chat, live scores) | ✅ WebSockets / Firebase |
| Push notifications | ✅ FCM (Firebase Cloud Messaging) |
Setup
Add WorkManager to your build.gradle (app):
dependencies {
val work_version = "2.9.0"
implementation "androidx.work:work-runtime-ktx:$work_version"
}
Step 1 — Create a Worker
A CoroutineWorker is the Kotlin-friendly version of WorkManager's worker class. Override doWork() — everything you put in there runs on a background thread automatically:
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// Get input data passed from the WorkRequest
val userId = inputData.getString("userId") ?: return Result.failure()
// Do your background work here — runs off the main thread
val syncResult = syncUserData(userId)
if (syncResult.isSuccess) {
// Pass output data back to the observer
val output = workDataOf("syncedCount" to syncResult.count)
Result.success(output)
} else {
// Retry later — WorkManager will reschedule
Result.retry()
}
} catch (e: Exception) {
// Permanent failure — won't retry
Result.failure()
}
}
private suspend fun syncUserData(userId: String): SyncResult {
// Your actual sync logic here — API call, DB write, etc.
return SyncResult(isSuccess = true, count = 42)
}
}
data class SyncResult(val isSuccess: Boolean, val count: Int)
Result.success()— work completed, don't retryResult.retry()— something went wrong, reschedule with backoffResult.failure()— permanent failure, don't retry
Step 2 — Schedule a One-Time Sync
import androidx.work.*
import java.util.concurrent.TimeUnit
// Build input data to pass to the Worker
val inputData = workDataOf("userId" to "user_123")
// Define constraints — only run on network, not low battery
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
// Create the work request
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(inputData)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, // Wait longer between each retry
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.addTag("sync_work") // Tag for easy cancellation
.build()
// Enqueue — WorkManager handles everything from here
WorkManager.getInstance(context).enqueue(syncRequest)
Step 3 — Schedule Periodic Background Sync
For recurring sync — like refreshing a feed every hour — use PeriodicWorkRequest. The minimum interval is 15 minutes (Android OS enforces this):
val periodicSyncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.addTag("periodic_sync")
.build()
// Use KEEP to avoid scheduling duplicates if already enqueued
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"periodic_data_sync", // Unique name — prevents duplicates
ExistingPeriodicWorkPolicy.KEEP, // Keep existing if already scheduled
periodicSyncRequest
)
Step 4 — Observe Work Status in Your ViewModel
WorkManager exposes a LiveData of WorkInfo so you can observe work status and update your UI when the sync completes:
class SyncViewModel(application: Application) : AndroidViewModel(application) {
private val workManager = WorkManager.getInstance(application)
// Observe all work tagged "sync_work"
val syncWorkInfo: LiveData<List<WorkInfo>> =
workManager.getWorkInfosByTagLiveData("sync_work")
fun startSync(userId: String) {
val inputData = workDataOf("userId" to userId)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(inputData)
.setConstraints(constraints)
.addTag("sync_work")
.build()
workManager.enqueue(request)
}
fun cancelSync() {
workManager.cancelAllWorkByTag("sync_work")
}
}
// In your Fragment
class SyncFragment : Fragment() {
private val viewModel: SyncViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.syncWorkInfo.observe(viewLifecycleOwner) { workInfoList ->
val workInfo = workInfoList.firstOrNull() ?: return@observe
when (workInfo.state) {
WorkInfo.State.ENQUEUED -> showStatus("Sync scheduled...")
WorkInfo.State.RUNNING -> showStatus("Syncing...")
WorkInfo.State.SUCCEEDED -> {
val count = workInfo.outputData.getInt("syncedCount", 0)
showStatus("Synced $count items ✓")
}
WorkInfo.State.FAILED -> showStatus("Sync failed")
WorkInfo.State.CANCELLED -> showStatus("Sync cancelled")
WorkInfo.State.BLOCKED -> showStatus("Waiting for constraints...")
}
}
binding.btnSync.setOnClickListener {
viewModel.startSync("user_123")
}
}
}
Chaining Workers — Run Tasks in Sequence
One of WorkManager's most powerful features is chaining. You can run workers sequentially where output from one is input to the next, or in parallel:
// Sequential chain — each worker runs after the previous succeeds
WorkManager.getInstance(context)
.beginWith(OneTimeWorkRequestBuilder<FetchDataWorker>().build())
.then(OneTimeWorkRequestBuilder<ProcessDataWorker>().build())
.then(OneTimeWorkRequestBuilder<UploadReportWorker>().build())
.enqueue()
// Parallel then merge — A and B run simultaneously, C runs after both finish
val workA = OneTimeWorkRequestBuilder<FetchUserWorker>().build()
val workB = OneTimeWorkRequestBuilder<FetchSettingsWorker>().build()
val workC = OneTimeWorkRequestBuilder<MergeAndSyncWorker>().build()
WorkManager.getInstance(context)
.beginWith(listOf(workA, workB))
.then(workC)
.enqueue()
Best Practices
- Always use enqueueUniqueWork for periodic tasks — without a unique name, every app launch schedules a new instance and you end up with dozens of duplicate sync jobs running simultaneously.
- Use EXPONENTIAL backoff for network tasks — if the server is down, exponential backoff prevents your app from hammering it with retries. Start small and double the wait time each attempt.
- Set network constraints — always add
setRequiredNetworkType(NetworkType.CONNECTED)for any sync work. WorkManager queues the work and runs it automatically when connectivity is restored. - Use CoroutineWorker over Worker —
CoroutineWorkerrunsdoWork()on a background dispatcher automatically. The olderWorkerclass runs on the main thread by default which can cause ANRs. - Keep doWork() idempotent — WorkManager may run your worker more than once due to retries. Make sure running it twice doesn't cause duplicate data or side effects.
- Tag your work requests — tags make it easy to observe, cancel, or query specific work without keeping a reference to the work ID.
Frequently Asked Questions
What replaced SyncAdapter in Android?
WorkManager is the modern replacement for background sync tasks that must complete even if the app is closed. For foreground data fetching while the user is active, Retrofit with Kotlin Coroutines is the recommended approach.
What is the minimum interval for PeriodicWorkRequest?
The minimum repeat interval is 15 minutes — enforced by the Android OS to preserve battery life. For more frequent updates, use a foreground service or real-time solutions like WebSockets or Firebase.
Does WorkManager survive device reboot?
Yes — WorkManager persists scheduled work in a local database. If the device restarts, WorkManager reschedules all pending work automatically.
What is the difference between OneTimeWorkRequest and PeriodicWorkRequest?
OneTimeWorkRequest runs once and completes. PeriodicWorkRequest runs repeatedly at a defined interval (minimum 15 minutes). Use OneTimeWorkRequest for triggered tasks like file uploads. Use PeriodicWorkRequest for recurring sync like refreshing a feed.
- Retrofit with Kotlin Coroutines — The Complete Guide
- Kotlin Coroutines for Android — Suspend Functions, Scopes and Dispatchers
- ViewModel State Management — LiveData, StateFlow & SavedStateHandle
- Android ViewModel Example — Build Your First ViewModel
- Learn Android Development with Kotlin — Beginner's Guide
- WorkManager replaces SyncAdapter, AlarmManager, and JobScheduler for guaranteed background work
- Use CoroutineWorker over Worker — runs on background thread automatically
- Return Result.success(), Result.retry(), or Result.failure() from doWork()
- Use enqueueUniquePeriodicWork to prevent duplicate sync jobs
- Minimum periodic interval is 15 minutes — OS enforced
- WorkManager survives device reboot — work is persisted in local DB
- Use constraints — network, battery, storage — to run work only when safe
- Keep doWork() idempotent — it may run more than once due to retries