Background Tasks (WorkManager)

Medium PriorityAsked in ~45% of senior interviews

4 min read

Platform Integration

TaskToolNotes
CPU-bound work while app is openIsolate.run() / compute()Pure Dart
Periodic sync (every N hours)workmanagerWraps WorkManager (Android) + BGTaskScheduler (iOS)
Background fetch on scheduleworkmanager or background_fetchOS decides actual timing
Continuous locationgeolocator + foreground service / flutter_background_geolocationBattery-aware
Music / podcast playbackaudio_serviceOS-aware playback session
Silent push for data syncfirebase_messaging.onBackgroundMessageBrief window, must finish fast
One-shot deferred workOS scheduler primitives via pluginsNo long guarantees

iOS reality: background execution is opportunistic. The OS may run your task in 15 minutes, in 6 hours, or not until the user opens the app. Build for unreliability.


Code in action — WorkManager periodic sync

import 'package:workmanager/workmanager.dart';

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

  await Workmanager().initialize(callbackDispatcher, isInDebugMode: kDebugMode);

  await Workmanager().registerPeriodicTask(
    'sync-data',
    'syncDataTask',
    frequency: const Duration(hours: 1),
    constraints: Constraints(
      networkType: NetworkType.connected,
      requiresBatteryNotLow: true,
    ),
    inputData: {'userId': 'user_123'},
  );

  runApp(const MyApp());
}

// MUST be top-level + annotated for AOT entry point
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((taskName, inputData) async {
    // Init what you need — Firebase, Hive, etc. — but keep it light
    await Firebase.initializeApp();

    switch (taskName) {
      case 'syncDataTask':
        try {
          final userId = inputData?['userId'] ?? '';
          await SyncService.run(userId);
          return true;            // success
        } catch (_) {
          return false;           // ask system to retry later
        }
      default:
        return false;
    }
  });
}

Silent push for data sync (FCM)

@pragma('vm:entry-point')
Future<void> onBackgroundMessage(RemoteMessage msg) async {
  await Firebase.initializeApp();                      // isolated isolate; init again
  // Brief window — finish fast
  await DataSync.handle(msg.data);
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
  runApp(const MyApp());
}

Platform constraints to internalise

PlatformPeriodic frequencyWindowKilled-app behaviour
Android (WorkManager)Minimum 15 minutes10 min per taskTasks survive app close & device reboot
iOS (BGTaskScheduler)OS decides (could be hours)~30 secondsTasks may not run if app is force-quit
iOS silent pushOn notification arrival~30 secondsThrottled by OS; not guaranteed delivery

The honest answer to "Will my task run every hour?": on Android, probably yes. On iOS, maybe never. Design for both.


Common mistakes to avoid

❌ Expecting iOS to run your background task on a strict schedule
   It won't. The OS optimizes for battery; your task is one of many.
   ✅ Design tasks as idempotent and time-tolerant. Show data on next open.

❌ Heavy initialization in the background callback
   Background isolate has its own VM — re-initializes Firebase, etc.
   Long init = task killed before doing useful work.
   ✅ Minimize init; cache lookups in shared prefs.

❌ Forgetting @pragma('vm:entry-point')
   AOT tree-shaking strips your callback → "callback not found at runtime."

❌ Touching widget tree / BuildContext from a background task
   No tree exists in the background isolate.

❌ Trying to update UI directly from background
   Use SharedPreferences, Hive, etc. to persist; UI reads on next foreground.

❌ Battery / network constraints not declared
   Task drains battery, users uninstall.
   ✅ Set Constraints.networkType, requiresBatteryNotLow, requiresCharging if appropriate.

Interview follow-ups

  1. Why are iOS background tasks so unreliable? iOS prioritises battery and user experience. The OS gives background tasks opportunistic windows based on usage patterns, charging state, and network availability. There's no guarantee. Even Apple's own apps rely on push or user open for fresh data — embrace that model.

  2. What's the difference between compute and Isolate.run? compute(fn, arg) is the older API — spawns an isolate, runs fn, returns. Isolate.run (Dart 2.19+) is the modern equivalent: ergonomic, supports closures, similar semantics. Both run on a separate isolate so the UI thread stays responsive. Use them for parsing big JSON, image processing, anything CPU-heavy.

  3. How do you handle "task A finished, run task B" patterns in WorkManager? WorkManager has chained / dependent work APIs. The Flutter workmanager plugin's surface is simpler — you typically chain by scheduling B from within A's callback once it succeeds. For complex DAGs, drop into native WorkManager.

  4. How do you test a background callback? You don't — directly. You test the function it calls (SyncService.run) with unit tests. The plugin integration is wired up at platform level; verify it works manually or with integration tests on a real device. The callback itself should be a 1-line dispatcher.


How helpful was this content?

Please sign in to rate this article.