skills/workmanager/SKILL.md
Android WorkManager for guaranteed background execution - use for deferred tasks, periodic syncs, file uploads, notifications, and task chains. Covers CoroutineWorker, constraints, chaining, testing, and troubleshooting. Use when implementing background work that needs reliable execution across app restarts and doze mode.
npx skillsauth add andvl1/claude-plugin workmanagerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
WorkManager is the recommended solution for persistent, guaranteed background work on Android.
Use WorkManager for:
Don't use WorkManager for:
// build.gradle.kts (androidMain or Android module)
dependencies {
implementation("androidx.work:work-runtime-ktx:2.10.0")
}
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
}
}
}
}
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"
}
}
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)
}
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
}
}
// 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)
// 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()
val delayedRequest = OneTimeWorkRequestBuilder<NotificationWorker>()
.setInitialDelay(1, TimeUnit.HOURS)
.build()
WorkManager.getInstance(context).enqueue(delayedRequest)
// For important user-facing work
val expeditedRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context).enqueue(expeditedRequest)
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()
See references/chaining.md for advanced chaining patterns and parallel execution.
WorkManager.getInstance(context)
.beginWith(downloadRequest)
.then(processRequest)
.then(uploadRequest)
.enqueue()
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()
WorkManager.getInstance(context)
.enqueueUniqueWork(
"daily_sync",
ExistingWorkPolicy.REPLACE,
syncRequest
)
WorkManager.getInstance(context)
.enqueueUniqueWork(
"upload_${fileId}",
ExistingWorkPolicy.KEEP,
uploadRequest
)
WorkManager.getInstance(context)
.enqueueUniqueWork(
"sync_chain",
ExistingWorkPolicy.APPEND,
newSyncRequest
)
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"periodic_sync",
ExistingPeriodicWorkPolicy.KEEP,
periodicRequest
)
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")
}
}
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")
}
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkLiveData("daily_sync")
.observe(lifecycleOwner) { workInfos ->
// Handle work info list
}
WorkManager.getInstance(context)
.getWorkInfoByIdFlow(workId)
.collect { workInfo ->
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> handleSuccess()
WorkInfo.State.FAILED -> handleFailure()
else -> {}
}
}
// 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()
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())
}
// 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()
}
}
}
CoroutineWorker for suspend functionssetBackoffCriteria() for custom retry strategiesResult.retry() for transient failuressetForeground() for long-running work (>10 min)See references/troubleshooting.md for detailed solutions to common problems.
Common issues:
development
Effective Go patterns — idiomatic code, testing, benchmarks, project layout. Always use Go 1.21+ patterns.
development
Go microservices — gRPC, REST, cloud-native patterns, service discovery, circuit breakers, observability, health checks, graceful shutdown.
development
Go concurrency mastery — goroutines, channels, context, sync primitives, patterns, performance.
development
Telegram Mini Apps development - use for building Mini App frontend, WebApp API, initData authentication, and Telegram integration