Background Tasks (WorkManager)
4 min read
Platform Integration
| Task | Tool | Notes |
|---|---|---|
| CPU-bound work while app is open | Isolate.run() / compute() | Pure Dart |
| Periodic sync (every N hours) | workmanager | Wraps WorkManager (Android) + BGTaskScheduler (iOS) |
| Background fetch on schedule | workmanager or background_fetch | OS decides actual timing |
| Continuous location | geolocator + foreground service / flutter_background_geolocation | Battery-aware |
| Music / podcast playback | audio_service | OS-aware playback session |
| Silent push for data sync | firebase_messaging.onBackgroundMessage | Brief window, must finish fast |
| One-shot deferred work | OS scheduler primitives via plugins | No 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
| Platform | Periodic frequency | Window | Killed-app behaviour |
|---|---|---|---|
| Android (WorkManager) | Minimum 15 minutes | 10 min per task | Tasks survive app close & device reboot |
| iOS (BGTaskScheduler) | OS decides (could be hours) | ~30 seconds | Tasks may not run if app is force-quit |
| iOS silent push | On notification arrival | ~30 seconds | Throttled 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
-
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.
-
What's the difference between
computeandIsolate.run?compute(fn, arg)is the older API — spawns an isolate, runsfn, 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. -
How do you handle "task A finished, run task B" patterns in WorkManager? WorkManager has chained / dependent work APIs. The Flutter
workmanagerplugin'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. -
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.