androidengineers.Book a session

Data Synchronization

WorkManager: Constraints & Chaining

article20 minMedium

WorkManager is Android's recommended API for deferrable background work that must run even if the app is killed or the device restarts. It's built on JobScheduler, AlarmManager, and BroadcastReceiver internally, selecting the right mechanism per API level automatically.

Core Concepts

  • OneTimeWorkRequest — runs once; can be retried
  • PeriodicWorkRequest — repeats on an interval (minimum 15 minutes)
  • Constraints — preconditions (network, charging, battery, storage)
  • Chaining — sequence of workers where output feeds input
  • Unique Work — deduplicate by name to prevent concurrent duplicate jobs

Basic CoroutineWorker

class UploadArticleWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val articleId = inputData.getString("article_id") ?: return Result.failure()

        return try {
            val article = dao.getById(articleId) ?: return Result.failure()
            api.uploadArticle(article)
            Result.success()
        } catch (e: IOException) {
            if (runAttemptCount < 3) Result.retry() else Result.failure()
        }
    }
}

Constraints

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)   // need any network
    .setRequiresBatteryNotLow(true)                   // don't run on low battery
    .setRequiresCharging(false)                       // can run without charger
    .setRequiresStorageNotLow(true)                   // don't run when storage is critical
    .build()

val uploadRequest = OneTimeWorkRequestBuilder<UploadArticleWorker>()
    .setInputData(workDataOf("article_id" to article.id))
    .setConstraints(constraints)
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .addTag("upload")
    .build()

WorkManager.getInstance(context).enqueue(uploadRequest)

Chaining Workers

Chain sequentially for pipelines where each step depends on the previous:

// Step 1: Compress image → Step 2: Upload → Step 3: Notify server
val compressWork = OneTimeWorkRequestBuilder<CompressImageWorker>()
    .setInputData(workDataOf("image_path" to path))
    .build()

val uploadWork = OneTimeWorkRequestBuilder<UploadImageWorker>().build()

val notifyWork = OneTimeWorkRequestBuilder<NotifyServerWorker>().build()

WorkManager.getInstance(context)
    .beginWith(compressWork)
    .then(uploadWork)
    .then(notifyWork)
    .enqueue()

Pass data between workers via outputData:

class CompressImageWorker(...) : CoroutineWorker(...) {
    override suspend fun doWork(): Result {
        val inputPath = inputData.getString("image_path") ?: return Result.failure()
        val outputPath = compress(inputPath)
        return Result.success(workDataOf("compressed_path" to outputPath))
    }
}

class UploadImageWorker(...) : CoroutineWorker(...) {
    override suspend fun doWork(): Result {
        val path = inputData.getString("compressed_path") ?: return Result.failure()
        // ... upload the compressed file
        return Result.success()
    }
}

Parallel Fan-out then Merge

val downloadA = OneTimeWorkRequestBuilder<DownloadAWorker>().build()
val downloadB = OneTimeWorkRequestBuilder<DownloadBWorker>().build()
val mergeWork = OneTimeWorkRequestBuilder<MergeWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(downloadA, downloadB))  // run in parallel
    .then(mergeWork)                          // wait for both, then merge
    .enqueue()

Unique Work

Prevent duplicate syncs:

WorkManager.getInstance(context).enqueueUniqueWork(
    "article_sync",                        // unique name
    ExistingWorkPolicy.KEEP,              // KEEP running, REPLACE with new, or APPEND
    OneTimeWorkRequestBuilder<SyncWorker>().build()
)

// For periodic:
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "daily_sync",
    ExistingPeriodicWorkPolicy.KEEP,
    PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.DAYS).build()
)

Observing Work State

WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(uploadRequest.id)
    .observe(viewLifecycleOwner) { info ->
        when (info?.state) {
            WorkInfo.State.ENQUEUED -> showQueued()
            WorkInfo.State.RUNNING -> showProgress()
            WorkInfo.State.SUCCEEDED -> showSuccess()
            WorkInfo.State.FAILED -> showError()
            WorkInfo.State.CANCELLED -> showCancelled()
            else -> Unit
        }
    }

Key Takeaways

ConceptRule
CoroutineWorkerPrefer over Worker for Kotlin projects
Result.retry()Check runAttemptCount < N before retrying forever
ConstraintsAlways set NetworkType.CONNECTED for network-dependent work
Unique workUse enqueueUniqueWork to avoid duplicate background jobs
ChainingbeginWith().then().then().enqueue() for sequential pipelines
Periodic minimum15 minutes is the WorkManager minimum interval

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
WorkManager: Constraints & Chaining | Android System Design | Android Engineers