ager:** All deferrable, guaranteed background work must use WorkManager. 2. **CoroutineWorkeroverWorker:** Use CoroutineWorkerto leverage Kotlin coroutines, structured concurrency, and cancellation handling. 3. **Foreground Services for User-Facing Long Tasks:** Only useForegroundServicefor tasks the user is actively aware of and requires immediate completion (e.g., music playback, navigation, large file upload initiated by user). 4. **Expedited Jobs for Critical Sync:** UsesetExpedited()` for work that must run immediately even if constraints aren't met, provided it promotes to a foreground service.
Step-by-Step Implementation
1. Define the Worker with Constraints
Create a CoroutineWorker that handles the business logic. This worker runs on a background thread pool managed by WorkManager.
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.WorkResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
class DataSyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
// Simulate network operation
val url = URL("https://api.example.com/sync")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.connect()
return@withContext if (connection.responseCode == 200) {
Result.success()
} else {
Result.retry()
}
} catch (e: Exception) {
// Exponential backoff is handled automatically by WorkManager
Result.retry()
}
}
}
2. Enqueue with Constraints
Configure the work request with constraints. This is where battery optimization happens. The system will defer execution until these conditions are met.
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Constraints
import androidx.work.NetworkType
fun enqueueDataSync(workManager: WorkManager) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // Wi-Fi only
.setRequiresBatteryNotLow(true)
.setRequiresCharging(false)
.build()
val syncWork = OneTimeWorkRequestBuilder<DataSyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
// REPLACE ensures only one sync runs at a time
workManager.enqueueUniqueWork(
"data_sync_unique",
ExistingWorkPolicy.REPLACE,
syncWork
)
}
3. Implement ForegroundService for Immediate Critical Tasks
For tasks requiring immediate execution that survive Doze, implement a ForegroundService. This requires a notification and specific manifest declarations.
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class UploadService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notification = createNotification()
// API 34 requires startForeground to be called within 5 seconds
startForeground(1, notification)
serviceScope.launch {
try {
performUpload()
// Stop service when done
stopSelf()
} finally {
// Ensure service stops even on failure
stopSelf()
}
}
return START_NOT_STICKY
}
private suspend fun performUpload() {
// Heavy upload logic
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
serviceScope.cancel()
super.onDestroy()
}
}
4. Manifest Configuration (API 34 Compliance)
Android 14 mandates explicit service types. Failure to declare these results in SecurityException.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT" />
<application>
<service
android:name=".UploadService"
android:foregroundServiceType="dataSync|fileManagement"
android:exported="false" />
</application>
</manifest>
Pitfall Guide
Mistake: Enqueuing a WorkRequest to update a UI element immediately after a user action.
Impact: WorkManager is a deferrable scheduler. The system may delay execution by seconds or minutes based on load. The UI will appear frozen or unresponsive.
Fix: Use Kotlin Coroutine with ViewModelScope for immediate tasks. Reserve WorkManager for work that can survive process death.
2. Ignoring API 34 FOREGROUND_SERVICE_TYPE
Mistake: Adding FOREGROUND_SERVICE permission but omitting the specific type permission (e.g., FOREGROUND_SERVICE_LOCATION).
Impact: App crashes with SecurityException on Android 14+. Google Play rejects updates lacking these declarations.
Fix: Audit all foreground services and add the corresponding type permissions and manifest attributes. Use foregroundServiceType in the manifest.
3. Blocking doWork() with CPU-Intensive Operations
Mistake: Running heavy image processing or complex calculations directly in doWork() without context switching.
Impact: While Worker runs off the main thread, WorkManager uses a limited thread pool. Blocking this pool delays other workers and can trigger watchdog timeouts.
Fix: Use CoroutineWorker and withContext(Dispatchers.Default) for CPU-bound work. Ensure long-running work yields periodically.
4. Forgetting setExpedited() for Critical Syncs
Mistake: Using standard WorkRequest for tasks that users expect to complete immediately (e.g., sending a message draft).
Impact: The task sits in the queue until constraints are met, causing data loss perception.
Fix: Use setExpedited() for work that must run immediately. Note that this requires a foreground service notification, so use only when necessary.
5. Memory Leaks in ForegroundService
Mistake: Holding references to Activity or View contexts in a Service.
Impact: The service outlives the activity, preventing garbage collection and causing OOM errors.
Fix: Use applicationContext within services. Ensure CoroutineScope is cancelled in onDestroy().
6. Improper Retry Logic
Mistake: Returning Result.retry() without configuring backoff, or returning Result.failure() for transient errors.
Impact: Result.retry() with no backoff causes tight loops draining battery. Result.failure() drops data on network blips.
Fix: Always configure setBackoffCriteria(). Return Result.failure() only for non-recoverable errors (e.g., auth token expired).
7. Testing Without Doze Mode Simulation
Mistake: Validating background work only with the device plugged in and screen on.
Impact: Work behaves correctly in dev but fails in production when the device enters Doze.
Fix: Use adb shell dumpsys deviceidle force-idle to simulate Doze. Verify work execution using adb shell cmd jobscheduler run.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Periodic data sync (e.g., every 6 hours) | WorkManager (PeriodicWorkRequest) | System-managed batching, battery efficient, survives reboots. | Low |
| User uploads a photo while app is backgrounded | ForegroundService + WorkManager Expedited | Immediate execution required; user expects completion; notification provides transparency. | Medium |
| Process analytics events | WorkManager (OneTimeWorkRequest) | Deferrable, guaranteed delivery, no UI impact. | Low |
| Real-time location tracking for navigation | ForegroundService (Location type) | High accuracy, survives Doze, complies with location policies. | High |
| Cleanup cache files on idle | WorkManager with setRequiresDeviceIdle(true) | Runs only when device is idle, zero impact on user experience. | Low |
Configuration Template
build.gradle.kts Dependencies:
dependencies {
// WorkManager Kotlin Extensions
implementation("androidx.work:work-runtime-ktx:2.9.0")
// Coroutines for async work
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
AndroidManifest.xml Snippet:
<manifest ...>
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application ...>
<!-- Foreground Service Declaration -->
<service
android:name=".core.sync.SyncForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<!-- WorkManager Configuration (Optional: Custom Config) -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>
Quick Start Guide
- Add Dependencies: Include
work-runtime-ktx in your module's build.gradle. Sync the project.
- Create Worker: Implement a class extending
CoroutineWorker. Override doWork() and return Result.success(), Result.failure(), or Result.retry().
- Enqueue Work: Obtain
WorkManager.getInstance(context). Build a OneTimeWorkRequest with desired Constraints and call enqueue().
- Verify Execution: Run the app. Use
adb shell cmd jobscheduler run -f <package> <job_id> to force immediate execution of pending work for testing. Check logs for worker execution.
- Handle Results: Observe
WorkInfo via workManager.getWorkInfoByIdLiveData(workId) to update UI or trigger subsequent actions upon completion.