MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Android has multiple ways to run background work. Choosing wrong leads to work that gets killed by Doze mode, battery drain, or ANRs. Here's when to use WorkManager, coroutines, foreground services, and how each interacts with the system.
On this page
Android's background processing rules have tightened significantly across OS versions. Work that "just ran in a thread" on Android 5 gets killed by Doze mode on Android 10+. The right tool depends on the nature of the work.
Is the work user-initiated and currently visible to the user?
├── Yes → Coroutines in ViewModel (tied to UI lifecycle)
└── No
Does the work need guaranteed completion (must finish even if app is killed)?
├── Yes → WorkManager
└── No
Is the work long-running and user-visible (like music playback, file upload)?
├── Yes → ForegroundService
└── No → Coroutines in a non-UI scopeMost work that starts from user action and completes while the screen is visible:
class TaskViewModel : ViewModel() {
fun loadTasks() {
viewModelScope.launch {
// Automatically cancelled when ViewModel is destroyed
val tasks = repository.getAllTasks()
_uiState.value = TaskUiState.Success(tasks)
}
}
fun syncTasks() {
viewModelScope.launch {
_uiState.value = TaskUiState.Loading
try {
taskSyncService.sync()
_uiState.value = TaskUiState.Success(repository.getAllTasks())
} catch (e: Exception) {
_uiState.value = TaskUiState.Error(e.message ?: "Sync failed")
}
}
}
}viewModelScopeFor work that should outlive the ViewModel but not need guaranteed completion, use
applicationScopeclass MyApplication : Application() {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
}WorkManager runs work even if the app is closed or the device restarts. Use it for:
class UploadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val fileUri = inputData.getString("file_uri") ?: return Result.failure()
return try {
uploadService.upload(fileUri)
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry()
else Result.failure(workDataOf("error" to e.message))
}
}
}
// Schedule
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(workDataOf("file_uri" to fileUri.toString()))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("file_upload_$fileId", ExistingWorkPolicy.KEEP, uploadRequest)enqueueUniqueWorkWork that's user-visible (music player, file download, GPS tracking) needs a
ForegroundServiceclass MusicPlaybackService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildPlaybackNotification()
startForeground(NOTIFICATION_ID, notification)
// Start the actual work
mediaPlayer.start()
return START_STICKY // Restart if killed
}
private fun buildPlaybackNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_music)
.setContentTitle("Playing: ${currentTrack.title}")
.addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
.build()
}
}The notification shows users the work is running. Without a visible notification, the foreground service won't survive.
Android's battery optimization restricts background work when the device is idle:
Doze Mode (device stationary and unplugged):
App Standby:
WorkManager handles both — it uses JobScheduler internally, which respects Doze while ensuring work runs when the device wakes up.
What still runs in Doze:
setExpedited()For urgent background work:
val urgentWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()Running long tasks in onReceive()
goAsync()Using Thread.sleep()
delay()Scheduling too much periodic work. Every periodic WorkManager task runs regardless of whether there's anything to do. Use interval wisely — most apps don't need more frequent than 15 minutes.
Not handling Result.failure()
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadRequest.id)
.observe(this) { workInfo ->
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> showSuccess()
WorkInfo.State.FAILED -> showError("Upload failed")
WorkInfo.State.RUNNING -> showProgress()
else -> Unit
}
}| Use Case | Tool |
|---|---|
| User-triggered, completes with screen | code |
| Must complete even if app closes | code |
| Long-running, user-visible | code |
| Periodic background sync | code |
| Reacts to system events (boot, connectivity change) | code code |
viewModelScopeBroadcastReceiver.onReceive()Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read