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