Background Fetch and the Workmanager Plugin
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.
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
}
@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, ornot_requiredrequiresBatteryNotLow: defer while battery is below system thresholdrequiresCharging: only run while plugged inrequiresDeviceIdle: 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
BGAppRefreshTaskwith 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.plistunderBGTaskSchedulerPermittedIdentifiers.
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.