workmanager

📁 andvl1/claude-plugin 📅 4 days ago
4
总安装量
2
周安装量
#52772
全站排名
安装命令
npx skills add https://github.com/andvl1/claude-plugin --skill workmanager

Agent 安装分布

opencode 2
gemini-cli 2
claude-code 2
github-copilot 2
codex 2
kimi-cli 2

Skill 文档

Android WorkManager

WorkManager is the recommended solution for persistent, guaranteed background work on Android.

When to Use WorkManager

Use WorkManager for:

  • Periodic background sync – Sync data with server every 15+ minutes
  • Deferred tasks – Upload files, compress images when device is ready
  • Guaranteed execution – Tasks that must run even if app is killed
  • Constraint-based work – Run only when WiFi connected, battery charging, etc.

Don’t use WorkManager for:

  • Immediate execution – Use Kotlin coroutines directly
  • Precise timing – Use AlarmManager for exact scheduling
  • Foreground work – Use coroutines in ViewModel/Service

Dependencies

// build.gradle.kts (androidMain or Android module)
dependencies {
    implementation("androidx.work:work-runtime-ktx:2.9.0")
}

CoroutineWorker Basics

Simple Worker

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

    override suspend fun doWork(): Result {
        return try {
            // Perform background work
            val data = fetchDataFromServer()
            saveToDatabase(data)

            Result.success()
        } catch (e: Exception) {
            Log.e("SyncWorker", "Sync failed", e)

            if (runAttemptCount < 3) {
                Result.retry() // Retry with exponential backoff
            } else {
                Result.failure() // Give up after 3 attempts
            }
        }
    }
}

Worker with Input/Output Data

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

    override suspend fun doWork(): Result {
        // Get input data
        val fileUri = inputData.getString(KEY_FILE_URI) ?: return Result.failure()
        val userId = inputData.getLong(KEY_USER_ID, -1L)

        return try {
            val uploadedUrl = uploadFile(fileUri)

            // Return output data
            val outputData = workDataOf(
                KEY_UPLOADED_URL to uploadedUrl,
                KEY_TIMESTAMP to System.currentTimeMillis()
            )

            Result.success(outputData)
        } catch (e: Exception) {
            Result.retry()
        }
    }

    companion object {
        const val KEY_FILE_URI = "file_uri"
        const val KEY_USER_ID = "user_id"
        const val KEY_UPLOADED_URL = "uploaded_url"
        const val KEY_TIMESTAMP = "timestamp"
    }
}

Worker with Progress

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

    override suspend fun doWork(): Result {
        val url = inputData.getString(KEY_URL) ?: return Result.failure()

        return try {
            downloadFile(url) { progress ->
                // Update progress (0-100)
                setProgress(workDataOf(KEY_PROGRESS to progress))
            }

            Result.success()
        } catch (e: Exception) {
            Result.failure()
        }
    }

    companion object {
        const val KEY_URL = "url"
        const val KEY_PROGRESS = "progress"
    }
}

// Observe progress
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(workId)
    .observe(lifecycleOwner) { workInfo ->
        val progress = workInfo?.progress?.getInt(DownloadWorker.KEY_PROGRESS, 0) ?: 0
        updateProgressBar(progress)
    }

Foreground Worker (with Notification)

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

    override suspend fun doWork(): Result {
        // Show notification for long-running work
        setForeground(createForegroundInfo())

        return try {
            performLongOperation()
            Result.success()
        } catch (e: Exception) {
            Result.failure()
        }
    }

    private fun createForegroundInfo(): ForegroundInfo {
        val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
            .setContentTitle("Processing")
            .setContentText("Processing your request...")
            .setSmallIcon(R.drawable.ic_notification)
            .setOngoing(true)
            .build()

        return ForegroundInfo(NOTIFICATION_ID, notification)
    }

    companion object {
        private const val CHANNEL_ID = "work_channel"
        private const val NOTIFICATION_ID = 1
    }
}

Scheduling Work

One-Time Work

// Simple enqueue
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
    .build()

WorkManager.getInstance(context).enqueue(syncRequest)

// With input data
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .setInputData(workDataOf(
        UploadWorker.KEY_FILE_URI to fileUri,
        UploadWorker.KEY_USER_ID to userId
    ))
    .build()

WorkManager.getInstance(context).enqueue(uploadRequest)

// With constraints (see Constraints section)
val constrainedRequest = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueue(constrainedRequest)

Periodic Work

// Minimum interval is 15 minutes
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    repeatInterval = 15,
    repeatIntervalTimeUnit = TimeUnit.MINUTES
)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueue(syncRequest)

// With flex interval (run within last 5 minutes of 15-minute period)
val flexRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    repeatInterval = 15,
    repeatIntervalTimeUnit = TimeUnit.MINUTES,
    flexTimeInterval = 5,
    flexTimeIntervalUnit = TimeUnit.MINUTES
)
    .build()

Delayed Work

val delayedRequest = OneTimeWorkRequestBuilder<NotificationWorker>()
    .setInitialDelay(1, TimeUnit.HOURS)
    .build()

WorkManager.getInstance(context).enqueue(delayedRequest)

Expedited Work (Android 12+)

// For important user-facing work
val expeditedRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(context).enqueue(expeditedRequest)

Constraints

See references/constraints.md for detailed constraint patterns and combinations.

Quick reference:

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED) // Any network
    .setRequiresBatteryNotLow(true)
    .setRequiresCharging(false)
    .setRequiresStorageNotLow(true)
    .setRequiresDeviceIdle(false) // API 23+
    .build()

val request = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(constraints)
    .build()

Work Chaining

See references/chaining.md for advanced chaining patterns and parallel execution.

Sequential Chain

WorkManager.getInstance(context)
    .beginWith(downloadRequest)
    .then(processRequest)
    .then(uploadRequest)
    .enqueue()

Parallel Chains

val chain1 = WorkManager.getInstance(context).beginWith(work1A).then(work1B)
val chain2 = WorkManager.getInstance(context).beginWith(work2A).then(work2B)

val finalWork = OneTimeWorkRequestBuilder<FinalWorker>().build()

WorkContinuation.combine(listOf(chain1, chain2))
    .then(finalWork)
    .enqueue()

Unique Work

Replace Existing Work

WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "daily_sync",
        ExistingWorkPolicy.REPLACE,
        syncRequest
    )

Keep Existing Work

WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "upload_${fileId}",
        ExistingWorkPolicy.KEEP,
        uploadRequest
    )

Append to Existing Work

WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "sync_chain",
        ExistingWorkPolicy.APPEND,
        newSyncRequest
    )

Unique Periodic Work

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "periodic_sync",
        ExistingPeriodicWorkPolicy.KEEP,
        periodicRequest
    )

Observing Work

By ID

WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(workRequest.id)
    .observe(lifecycleOwner) { workInfo ->
        when (workInfo?.state) {
            WorkInfo.State.ENQUEUED -> showStatus("Queued")
            WorkInfo.State.RUNNING -> showStatus("Running")
            WorkInfo.State.SUCCEEDED -> {
                showStatus("Success")
                val outputUrl = workInfo.outputData.getString(KEY_UPLOADED_URL)
                handleSuccess(outputUrl)
            }
            WorkInfo.State.FAILED -> showStatus("Failed")
            WorkInfo.State.BLOCKED -> showStatus("Blocked")
            WorkInfo.State.CANCELLED -> showStatus("Cancelled")
            null -> showStatus("Unknown")
        }
    }

By Tag

val request = OneTimeWorkRequestBuilder<SyncWorker>()
    .addTag("sync")
    .build()

WorkManager.getInstance(context)
    .getWorkInfosByTagLiveData("sync")
    .observe(lifecycleOwner) { workInfos ->
        val running = workInfos.count { it.state == WorkInfo.State.RUNNING }
        updateUI("$running sync tasks running")
    }

By Unique Name

WorkManager.getInstance(context)
    .getWorkInfosForUniqueWorkLiveData("daily_sync")
    .observe(lifecycleOwner) { workInfos ->
        // Handle work info list
    }

Using Flow (Coroutines)

WorkManager.getInstance(context)
    .getWorkInfoByIdFlow(workId)
    .collect { workInfo ->
        when (workInfo?.state) {
            WorkInfo.State.SUCCEEDED -> handleSuccess()
            WorkInfo.State.FAILED -> handleFailure()
            else -> {}
        }
    }

Cancelling Work

// Cancel by ID
WorkManager.getInstance(context).cancelWorkById(workId)

// Cancel by tag
WorkManager.getInstance(context).cancelAllWorkByTag("sync")

// Cancel by unique name
WorkManager.getInstance(context).cancelUniqueWork("daily_sync")

// Cancel all work
WorkManager.getInstance(context).cancelAllWork()

Testing

See references/testing.md for complete testing guide including TestWorkerFactory and WorkManagerTestInitHelper.

Quick test example:

@Test
fun testSyncWorker() = runTest {
    val context = ApplicationProvider.getApplicationContext<Context>()
    val executor = Executors.newSingleThreadExecutor()

    WorkManagerTestInitHelper.initializeTestWorkManager(context)

    val worker = TestListenableWorkerBuilder<SyncWorker>(context).build()

    val result = worker.doWork()

    assertThat(result).isEqualTo(Result.success())
}

Dependency Injection

With Metro DI

// Define factory in DI graph
@ModuleScope
class WorkerFactory(
    private val syncRepository: SyncRepository,
    private val uploadService: UploadService
) : WorkerFactory() {

    override fun createWorker(
        appContext: Context,
        workerClassName: String,
        workerParameters: WorkerParameters
    ): ListenableWorker? {
        return when (workerClassName) {
            SyncWorker::class.java.name ->
                SyncWorker(appContext, workerParameters, syncRepository)

            UploadWorker::class.java.name ->
                UploadWorker(appContext, workerParameters, uploadService)

            else -> null
        }
    }
}

// In Application onCreate
class MyApplication : Application(), Configuration.Provider {

    private lateinit var workerFactory: WorkerFactory

    override fun onCreate() {
        super.onCreate()

        // Get factory from DI
        workerFactory = AppGraph.workerFactory
    }

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .build()
}

// Worker with dependencies
class SyncWorker(
    context: Context,
    params: WorkerParameters,
    private val repository: SyncRepository
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            repository.sync()
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

Important Limitations

Timing Constraints

  • Minimum periodic interval: 15 minutes
  • Flex interval: Must be >= 5 minutes and < repeat interval
  • Not for exact timing: WorkManager is not AlarmManager – execution time is approximate
  • Doze mode delays: Work can be significantly delayed when device is in doze mode

Execution Constraints

  • 10-minute execution limit: Workers should complete within 10 minutes
  • Background execution limits: Subject to Android’s background execution limits
  • CoroutineWorker scope: Uses default Dispatchers.Default, not main thread
  • No guarantee of immediate execution: Work is scheduled optimally by system

Data Limitations

  • Input/Output data size: Limited to 10KB
  • Primitive types only: WorkData supports only primitives, arrays, and strings
  • No complex objects: Cannot pass custom objects directly
  • Use serialization: For complex data, serialize to JSON string or use file paths

Worker Lifecycle

  • Worker is recreated: Each execution creates a new Worker instance
  • No shared state: Cannot rely on instance variables between executions
  • Context is Application: Worker’s context is always Application context, not Activity
  • RunAttemptCount: Increments on each retry, resets on success

Device and System Constraints

  • Battery optimization: Can be affected by manufacturer-specific battery optimizations
  • App standby buckets: Work frequency limited by app’s standby bucket
  • Background restrictions: Some manufacturers aggressively kill background work
  • Requires Google Play Services: WorkManager relies on Google Play Services JobScheduler

Best Practices

Do’s

  • Use CoroutineWorker for suspend functions
  • Set appropriate constraints for network/battery requirements
  • Use setBackoffCriteria() for custom retry strategies
  • Tag work requests for easier management
  • Use unique work names to prevent duplicate tasks
  • Return Result.retry() for transient failures
  • Implement proper error handling and logging
  • Use setForeground() for long-running work (>10 min)

Don’ts

  • Don’t use WorkManager for immediate execution
  • Don’t expect precise scheduling
  • Don’t pass large data through WorkData (use file URIs)
  • Don’t block main thread in Worker
  • Don’t rely on Worker instance state
  • Don’t schedule periodic work < 15 minutes
  • Don’t forget to cancel work when no longer needed
  • Don’t use for foreground services (use actual Service)

Troubleshooting

See references/troubleshooting.md for detailed solutions to common problems.

Common issues:

  • Work not executing
  • Constraints not working as expected
  • Work retrying indefinitely
  • DI not working in Workers
  • Testing failures

Resources