Push Notifications & Background Services

Background Fetch and the Workmanager Plugin

16 min Lesson 8 of 11

Background Fetch and the Workmanager Plugin

Mobile operating systems aggressively restrict background execution to conserve battery and resources. Yet many real-world applications—news readers, messaging apps, fitness trackers—need to fetch data, sync state, or trigger local notifications even when the user is not actively using the app. The workmanager plugin bridges this gap by providing a unified Dart API that maps to WorkManager on Android and BGTaskScheduler on iOS, letting you schedule Dart code to run in the background reliably.

Why Background Execution Is Hard

On Android, processes that are not in the foreground can be killed at any time. On iOS, background execution time is tightly budgeted. The workmanager plugin solves this by registering tasks with the platform's own scheduler, which honours the task after the app is suspended or even after a device reboot (Android only). Tasks are not guaranteed to run at an exact time, but they are guaranteed to run given the right conditions.

Note: Background tasks managed by workmanager run in a separate Dart isolate. They do not share memory with the main UI isolate, so you cannot update widgets or call setState() directly from a background callback. Use local notifications or shared storage (e.g., shared_preferences) to communicate results back to the UI.

Adding the Dependency

Add workmanager to pubspec.yaml:

pubspec.yaml

dependencies:
  workmanager: ^0.5.2
  flutter_local_notifications: ^17.0.0  # optional, for result notifications

On Android, workmanager requires no additional setup beyond the dependency—the plugin's auto-configuration handles the WorkManager initialisation. On iOS, you must add the background fetch capability in Xcode (Signing & Capabilities → Background Modes → Background fetch) and register the task identifier in Info.plist.

The Top-Level Callback Constraint

The most important architectural rule of workmanager is that the callback function that executes your background logic must be a top-level Dart function (or a static method). It cannot be a closure, an anonymous lambda, or an instance method, because the background isolate is spawned fresh and cannot access any in-memory state from the previous app session.

Correct callback declaration

// TOP-LEVEL function — not inside a class, not a closure
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((taskName, inputData) async {
    switch (taskName) {
      case 'fetchLatestPosts':
        await _syncPosts(inputData);
        break;
      case 'cleanupCache':
        await _purgeOldCache();
        break;
    }
    return Future.value(true); // true = success, false = failure, retry
  });
}

Future<void> _syncPosts(Map<String, dynamic>? input) async {
  // Perform HTTP request, write to local DB, etc.
  final userId = input?['userId'] as String? ?? 'anonymous';
  print('Syncing posts for user: $userId');
  // ... network + storage logic
}

Future<void> _purgeOldCache() async {
  print('Purging old cache entries...');
  // ... file system or DB cleanup
}
Warning: The @pragma('vm:entry-point') annotation is required in release builds (when tree shaking is enabled) to prevent the Dart compiler from eliminating the top-level function. Omitting it will cause a silent failure in production builds.

Initialising Workmanager

Call Workmanager().initialize() once, early in main(), before runApp(). Pass the top-level callback as the first argument. The optional isInDebugMode flag enables a system notification on Android whenever a background task fires, which is extremely useful during development.

main.dart — initialisation

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Workmanager().initialize(
    callbackDispatcher,   // the top-level function defined above
    isInDebugMode: kDebugMode,
  );

  runApp(const MyApp());
}

// Schedule tasks after initialisation — e.g., after login:
Future<void> scheduleBackgroundWork() async {
  // One-off task: runs once as soon as conditions allow
  await Workmanager().registerOneOffTask(
    'oneoff-sync-001',          // unique task name
    'fetchLatestPosts',         // taskName passed to callbackDispatcher
    inputData: {'userId': 'user_42'},
    constraints: Constraints(
      networkType: NetworkType.connected,
    ),
  );

  // Periodic task: repeats on a minimum interval of 15 minutes
  await Workmanager().registerPeriodicTask(
    'periodic-cache-cleanup',
    'cleanupCache',
    frequency: const Duration(hours: 1),
    constraints: Constraints(
      networkType: NetworkType.not_required,
      requiresBatteryNotLow: true,
    ),
  );
}

Task Return Values and Retry Logic

Your callback must return a Future<bool>:

  • true — task succeeded; workmanager marks it complete.
  • false — task failed; workmanager may retry based on backoff policy.
  • Throwing an unhandled exception is treated as failure.

You can customise the backoff policy when registering a task using the backoffPolicy and backoffPolicyDelay parameters to choose between BackoffPolicy.linear and BackoffPolicy.exponential.

Platform Constraints

The Constraints class expresses preconditions the scheduler must satisfy before launching the task:

  • networkType: NetworkType.connected, metered, unmetered, or not_required
  • requiresBatteryNotLow: defer while battery is below system threshold
  • requiresCharging: only run while plugged in
  • requiresDeviceIdle: Android only — Doze mode idle (API 23+)
  • requiresStorageNotLow: skip when internal storage is critically low

Cancelling and Managing Tasks

Use the cancellation API to stop scheduled work when it is no longer needed (e.g., on logout):

Cancelling tasks

// Cancel a specific task by its unique name
await Workmanager().cancelByUniqueName('periodic-cache-cleanup');

// Cancel all tasks registered by this app
await Workmanager().cancelAll();

iOS Limitations

On iOS, background execution is significantly more constrained than on Android:

  • Periodic tasks map to BGAppRefreshTask with a minimum interval of 15 minutes, but the actual frequency is determined by iOS heuristics based on the user's usage patterns.
  • One-off tasks map to BGProcessingTask, typically only run when the device is charging and on Wi-Fi.
  • Each task identifier must be declared in Info.plist under BGTaskSchedulerPermittedIdentifiers.
Tip: For time-sensitive background work on iOS (such as delivering a chat message while the app is backgrounded), prefer push notifications with a content-available payload combined with a UNNotificationServiceExtension, rather than relying solely on workmanager's periodic fetch.

Summary

The workmanager plugin lets you schedule both one-off and periodic background Dart tasks with a clean cross-platform API. The critical constraints to remember are: the callback must be a top-level function annotated with @pragma('vm:entry-point'), tasks run in their own isolated Dart isolate, return true for success and false for retry, and iOS imposes stricter scheduling policies than Android. Combine workmanager with local notifications or shared storage to surface background results to your users.