How it works
The explanation, with examples you can run.
WorkManager
class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result { val uri = inputData.getString("uri") ?: return Result.failure() return try { uploader.upload(uri) // suspend, main-safe Result.success() } catch (e: IOException) { Result.retry() // rescheduled with backoff } }} val request = OneTimeWorkRequestBuilder<UploadWorker>() .setInputData(workDataOf("uri" to uri.toString())) .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED)) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueueUniqueWork("upload-$id", ExistingWorkPolicy.KEEP, request)WorkManager writes the request to its own database, so it survives process death and reboots. It runs on JobScheduler underneath. Useful extras:
- Unique work stops duplicates when the user taps twice.
- Chains (
beginWith(a).then(b)) run steps in order. - Periodic work repeats no more often than every 15 minutes.
- Expedited work (
setExpedited) starts as soon as possible for short, user-important tasks, within a quota; on Android 11 and lower it is backed by a foreground service, so providegetForegroundInfo(). getWorkInfoByIdFlow(id)lets the UI show progress.
Because a worker may run more than once, make it idempotent: uploading the same photo twice should not create two posts.
Foreground services
Use one only when the user is aware of the task while it runs: playing media, navigation, a call, tracking a workout. It must show a notification, and:
- From Android 14 (API 34) every foreground service declares a type in the manifest (
mediaPlayback,location,dataSync,shortService, ...) and holds the matching permission. - From Android 12 (API 31) apps generally can't start a foreground service from the background, with listed exceptions such as a high-priority FCM message or an exact alarm.
- From Android 15 (API 35),
dataSyncandmediaProcessingservices get about 6 hours per 24 hours while the app is in the background, thenonTimeout()asks them to stop.
// After startForegroundService(intent), call this within 5 seconds:ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)For one-off user-initiated transfers on Android 14+, a user-initiated data transfer job (JobScheduler) is often a better fit than a dataSync service.
Doze and standby buckets
Doze (Android 6+) kicks in when the device is unplugged, still and screen-off for a while. Network access is cut and jobs, syncs and normal alarms wait for periodic maintenance windows, which get further apart over time. App Standby buckets (Android 9+) rank apps as active, working set, frequent, rare or restricted by how recently and often they are used; lower buckets get fewer job runs, alarms and network. The restricted bucket arrived in Android 12. These limits apply on battery only, and manufacturers can add their own on top.
Ways through, used sparingly: high-priority FCM messages, setExactAndAllowWhileIdle alarms (exact alarms need permission and, from Android 14, are denied by default for most new installs), and asking the user to exempt the app from battery optimisation, which Play allows only for specific use cases.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.