Skip to main content

Background Tasks & Notifications

TL;DR

Background Tasks: WorkManager (Android), BGTask (iOS) for reliable scheduling even after app kill. Respect system constraints (battery, network, storage).

Push Notifications: Firebase Cloud Messaging (Android), APNs (iOS). Sent from backend, wakes app, critical for engagement.

Geofencing: Location-based triggers; use carefully (battery impact significant). Monitor permissions (iOS restrictions).

Local Notifications: Scheduled without server; useful for reminders, alarms.

Learning Objectives

You will be able to:

  • Implement reliable background work that survives app termination.
  • Integrate push notifications from server to device.
  • Handle geofencing and location-based triggers responsibly.
  • Optimize battery consumption for background tasks.
  • Monitor permissions and handle user denials gracefully.

Motivating Scenario

Your fitness app tracks workouts and sends notifications. Users expect:

  • "Time for a run!" notification at scheduled times (even if app is closed)
  • Real-time push notifications from friends joining their run
  • Location-based alerts ("Great spot to run nearby")

Without proper background task handling:

  • Scheduling only works while app is open
  • Notifications require server polling (battery drain)
  • Location tracking drains battery in minutes

With proper implementation:

  • Tasks run on system schedule, respect battery
  • Push notifications wake app only when needed
  • Location features use geofencing (efficient) vs continuous tracking

Background Tasks

Android: WorkManager

Persistent work scheduling that survives app termination.

SyncWorker.kt
import androidx.work.Worker
import androidx.work.WorkerParameters

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

override fun doWork(): Result {
return try {
// Do work here (API calls, database updates)
val result = syncWithServer()
Result.success()
} catch (e: Exception) {
// Retry on failure
Result.retry()
}
}

private fun syncWithServer(): Boolean {
// Call API, update local database
return true
}
}

WorkManager handles:

  • Batching tasks for efficiency
  • Backoff + retry
  • Doze mode awareness (Android's battery saver)
  • Device reboot persistence

iOS: Background Tasks

BGTaskScheduler for periodic background processing.

BackgroundTasks.swift
import BackgroundTasks

class BackgroundTaskManager {
static func scheduleProcessingTask() {
let request = BGProcessingTaskRequest(identifier: "com.example.sync")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false // Allow on battery

do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Failed to schedule: \(error)")
}
}

// Handle when system triggers task
static func setupBackgroundHandlers() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.sync",
using: nil
) { task in
handleSyncTask(task as! BGProcessingTask)
}
}

static func handleSyncTask(_ task: BGProcessingTask) {
// Reschedule next task immediately
scheduleProcessingTask()

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1

let operation = BlockOperation {
// Do work: sync data, upload files
sleep(5) // Simulate work
}

operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}

task.expirationHandler = {
operation.cancel()
}

queue.addOperation(operation)
}
}

Key differences from Android:

  • No guarantee of execution (iOS suspends background work)
  • Requires Info.plist: BGTaskSchedulerPermittedIdentifiers
  • Limited to ~30 seconds execution time

Push Notifications

Android: Firebase Cloud Messaging (FCM)

Server sends message → FCM → Device → App.

MyFirebaseMessagingService.kt
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class MyFirebaseMessagingService : FirebaseMessagingService() {

// New token available (register with backend)
override fun onNewToken(token: String) {
super.onNewToken(token)
sendTokenToServer(token)
}

// Received message
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)

// Display notification
val title = message.notification?.title ?: "New message"
val body = message.notification?.body ?: ""

showNotification(title, body)

// Custom data handling
if (message.data.isNotEmpty()) {
val customData = message.data
handleCustomData(customData)
}
}

private fun showNotification(title: String, message: String) {
val notificationId = 1
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, intent, PendingIntent.FLAG_IMMUTABLE
)

val notification = NotificationCompat.Builder(this, "channel_id")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()

NotificationManagerCompat.from(this).notify(notificationId, notification)
}

private fun sendTokenToServer(token: String) {
// POST token to backend
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()

val api = retrofit.create(ApiService::class.java)
api.registerToken(TokenRequest(token)).enqueue(
object : Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {}
override fun onFailure(call: Call<Void>, t: Throwable) {}
}
)
}
}

iOS: Apple Push Notification Service (APNs)

Server sends → APNs → Device → App.

UserNotifications.swift
import UserNotifications

class NotificationManager {
static func requestPermissions() {
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .sound, .badge]
) { granted, error in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
}

// Receive notification while app in foreground
static func setupNotificationHandling() {
UNUserNotificationCenter.current().delegate = self
}
}

// Delegate methods
extension UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Register for remote notifications
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { _, _ in
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
return true
}

// Get device token (send to backend)
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
sendTokenToServer(token)
}

// Handle received notification while app is foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
@escaping (UNNotificationPresentationOptions) -> Void
) {
let userInfo = notification.request.content.userInfo
// Handle notification
completionHandler([.banner, .sound])
}
}

Patterns & Pitfalls

Pattern: Exponential Backoff

Retry failed tasks with increasing delays:

ExponentialBackoff.kt
val work = OneTimeWorkRequestBuilder<SyncWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS, // 15 seconds
TimeUnit.MILLISECONDS
)
.build()

// Retries: 15s → 30s → 60s → max 5 hours

Pitfall: Battery Drain from Location

Problem: Continuous location tracking uses 5-10% battery per hour.

Mitigation: Use geofencing (efficient) instead:

Geofencing.kt
val geofence = Geofence.Builder()
.setRequestId("workout_spot")
.setCircularRegion(40.7128, -74.0060, 500f) // 500m radius
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()

val request = GeofencingRequest.Builder()
.addGeofences(geofence)
.setInitialTrigger(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()

LocationServices.getGeofencingClient(context)
.addGeofences(request, geofencingPendingIntent)

Pitfall: Notification Spam

Problem: Sending too many notifications annoys users, they disable notifications.

Mitigation: Rate-limit, group notifications, respect quiet hours.

Design Review Checklist

  • Are background tasks respecting battery constraints?
  • Does app fail gracefully if permissions denied?
  • Is work idempotent (safe to retry)?
  • Are notifications actionable and timely?
  • Is notification frequency reasonable (not spam)?
  • Are tokens registered with backend after app install?
  • Do notifications work while app in background?
  • Is battery impact monitored (profiling tools)?
  • Are old tokens cleaned up (prevent stale tokens)?
  • Do tasks handle network failures (offline scenarios)?

When to Use / When Not to Use

Use Background Tasks When:

  • Periodic work (sync, refresh)
  • Work that can wait (non-urgent)
  • Respect battery constraints important

Use Push Notifications When:

  • Real-time, urgent alerts
  • Server-initiated messages
  • Need to wake app immediately

Use Geofencing When:

  • Location-based features
  • Efficiency more important than precision

Self-Check

  1. Why use WorkManager instead of just scheduling with Handler?
  2. How would you limit battery drain from background tasks?
  3. What's the difference between local and push notifications?

Next Steps

One Takeaway

ℹ️

Background tasks and notifications drive engagement, but must be implemented responsibly. Use WorkManager/BGTask for periodic work, respect battery constraints, and push notifications only for truly important messages. Monitor battery impact with profiling tools.

References

  1. Android WorkManager Documentation
  2. iOS Background Tasks Framework
  3. Firebase Cloud Messaging
  4. iOS UserNotifications Framework
  5. Android Geofencing