Add workmanager to your pubspec.yaml:
dependencies:
workmanager: ^0.10.0Then run:
flutter pub getAndroid works automatically - no additional setup required! ✅
If tasks stop running after the app is closed, that is usually the device's
battery optimizer, not the plugin. See the
Troubleshooting guide for per-vendor whitelist instructions,
constraint gotchas, and adb shell dumpsys jobscheduler verification.
iOS Minimum Deployment Target: iOS 14.0 or later is required. Update your project's deployment target in Xcode:
- Open
ios/Runner.xcodeprojin Xcode - Select the Runner target
- Set "Minimum Deployments" to iOS 14.0 or later
- Or edit
ios/Runner.xcodeproj/project.pbxprojand setIPHONEOS_DEPLOYMENT_TARGET = 14.0;
iOS requires a 5-minute setup in Xcode. Choose your approach based on your needs:
Background tasks run in a separate Flutter engine/isolate. Flutter plugins
(Firebase, shared_preferences, networking, etc.) are not registered in
that engine by default — calling them from inside your callbackDispatcher
fails with PlatformException(channel-error, Unable to establish connection on channel...).
To make plugins available in the background engine, wire the plugin
registrant callback in your AppDelegate.swift. The snippet below also follows
Flutter's UIScene lifecycle migration:
plugins are registered in didInitializeImplicitFlutterEngine (instead of
application(_:didFinishLaunchingWithOptions:)), and
WorkmanagerPlugin.registerLaunchHandlers() re-registers the BGTaskScheduler
launch handlers persisted from previous sessions — iOS requires those to be
registered before app launch finishes, and under UIScene the plugin's own
application callback runs too late for that:
import Flutter
import UIKit
import workmanager_apple
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
WorkmanagerPlugin.registerLaunchHandlers()
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}UIScene lifecycle: Apple requires UIKit apps to adopt the UIScene lifecycle
from the release following iOS 26. After migration, call
WorkmanagerPlugin.registerLaunchHandlers() in your AppDelegate's
application(_:didFinishLaunchingWithOptions:) so BGTaskScheduler launch
handlers are registered before app launch finishes (the plugin re-registers
every identifier it persisted in previous sessions, so tasks scheduled from
Dart keep being delivered after a relaunch). Apps that haven't migrated to
UIScene keep working without this call.
Background-isolate plugins: only plugins that are safe to use from a background isolate (no UI, no views) work there. Plugins that are not isolate-safe can still crash the background task.
For regular data sync, notifications, cleanup - uses iOS Background Fetch:
- Enable Background Modes in Xcode target capabilities (Configuration Guide) and add to Info.plist (UIBackgroundModes reference):
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>- No AppDelegate configuration needed - works automatically from Dart code
iOS Background Fetch scheduling: iOS completely controls when Background Fetch runs (typically once per day based on user app usage patterns). You cannot force immediate execution - it's designed for non-critical periodic updates like refreshing content.
For file uploads, data processing, longer tasks - uses BGTaskScheduler:
- Enable Background Modes in Xcode target capabilities (Configuration Guide) and add to Info.plist (UIBackgroundModes reference):
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.processing_task</string>
</array>- Configure AppDelegate.swift (required for BGTaskScheduler):
import workmanager_apple
// In application didFinishLaunching
WorkmanagerPlugin.registerBGProcessingTask(
withIdentifier: "com.yourapp.processing_task"
)Apps that adopted the UIScene lifecycle must also call
WorkmanagerPlugin.registerLaunchHandlers() in
application(_:didFinishLaunchingWithOptions:) — see the
registrant wiring section above.
iOS Task Identifier Matching: The task name in your Dart code must exactly match the identifier in Info.plist and AppDelegate. Using short names like "data_sync" in Dart while having com.yourapp.processing_task in native code will cause BGTaskSchedulerErrorDomain Code 3 errors.
Why BGTaskScheduler registration is needed: iOS requires every background task identifier to be listed in Info.plist for security and system resource management. The plugin registers the task handler automatically (at schedule time and again on the next app launch), so no manual AppDelegate code is required. Background Fetch (Option A) doesn't require this since it uses the simpler, system-managed approach.
For periodic tasks with more control than Background Fetch - uses BGTaskScheduler with frequency:
- Enable Background Modes in Xcode target capabilities (Configuration Guide) and add to Info.plist (UIBackgroundModes reference):
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.periodic_task</string>
</array>- (Optional) Configure AppDelegate.swift for custom frequency control:
import workmanager_apple
// In application didFinishLaunching
WorkmanagerPlugin.registerPeriodicTask(
withIdentifier: "com.yourapp.periodic_task",
frequency: NSNumber(value: 20 * 60) // 20 minutes (15 min minimum)
)The plugin re-registers launch handlers for scheduled task identifiers on app
launch, so this step is only needed if you want to pre-register the task or
control the scheduling hint from native code. Apps that adopted the UIScene
lifecycle must also call WorkmanagerPlugin.registerLaunchHandlers() in
application(_:didFinishLaunchingWithOptions:) — see the
registrant wiring section above.
iOS Task Identifier Matching: The task name in your Dart code must exactly match the identifier in Info.plist and AppDelegate. Using short names like "cleanup" in Dart while having com.yourapp.periodic_task in native code will cause BGTaskSchedulerErrorDomain Code 3 errors.
For apps participating in a Health Research Study — BGHealthResearchTaskRequest
gets additional priority/reliability for study-essential processing:
-
App requirements (Apple-enforced, outside the plugin):
- The app must be part of a HealthKit Health Research Study container
(typically provisioned with ResearchKit /
HKResearchStudy). - The
com.apple.developer.backgroundtasks.healthresearchentitlement must be present in your.entitlementsfile. - The user must have opted in to the study.
- The app must be part of a HealthKit Health Research Study container
(typically provisioned with ResearchKit /
-
Enable Background Modes and add the identifier to Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.health_research_task</string>
</array>- (Optional) Pre-register the launch handler in
AppDelegate.swift:
import workmanager_apple
// In application didFinishLaunching
WorkmanagerPlugin.registerBGHealthResearchTask(
withIdentifier: "com.yourapp.health_research_task"
)The plugin also registers the handler automatically at schedule time and on the next app launch.
- Schedule from Dart (iOS 17+; older iOS versions receive an error):
await Workmanager().registerHealthResearchTask(
'com.yourapp.health_research_task',
'healthResearchTask',
initialDelay: const Duration(hours: 1),
constraints: Constraints(
networkType: NetworkType.connected,
requiresCharging: true,
),
);Health research tasks require the entitlement. Without the Health Research
Study container and com.apple.developer.backgroundtasks.healthresearch
entitlement, BGTaskScheduler.submit fails and the task is never delivered —
the plugin cannot validate this for you. See the capability matrix in the docs
index for the full iOS strategy comparison.
For workloads that must begin immediately or shortly after submission and
are allowed to continue running while the app is backgrounded (e.g. ML
inference on a captured camera session) — BGContinuedProcessingTaskRequest.
The system shows a Live Activity to the user while the task runs.
- Enable Background Modes and add the identifier to Info.plist:
Continued-processing identifiers must use wildcard notation ending in
.*, with a prefix containing your app's bundle identifier:
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.continuedProcessing.*</string>
</array>- (Optional) Pre-register the launch handler in
AppDelegate.swift:
import workmanager_apple
// In application didFinishLaunching
WorkmanagerPlugin.registerBGContinuedProcessingTask(
withIdentifier: "com.yourapp.continuedProcessing.*"
)The plugin also registers the handler automatically at schedule time and on the next app launch.
- Schedule from Dart (iOS 26+; older iOS versions receive an error):
await Workmanager().registerContinuedProcessingTask(
'com.yourapp.continuedProcessing.*',
'continuedProcessingTask',
title: 'Example continued processing',
subtitle: 'Processing in progress',
inputData: {'frames': 120},
);Differences vs processing tasks: continued processing tasks start near
submission time (the scheduler ignores initialDelay) and are not limited to
idle devices, but the system still enforces expiration based on system
conditions and user input. Apple expects tasks to report progress
(NSProgress); the plugin does not currently plumb progress from Dart, so
callbacks that appear stalled may be expired by the scheduler.
Which option to choose?
- Option A (Background Fetch) for non-critical updates that can happen once daily (data sync, content refresh)
- Option B (BGTaskScheduler) for one-time tasks, file uploads, or immediate task scheduling
- Option C (Periodic Tasks) for regular tasks with custom frequency control (15+ minutes)
- Option D (Health Research Tasks) for iOS 17+ health research study apps that need reliable background processing
- Option E (Continued Processing Tasks) for iOS 26+ workloads that must start now and continue while backgrounded
macOS uses NSBackgroundActivityScheduler (the macOS equivalent of
BGTaskScheduler). No Info.plist keys or AppDelegate registration are needed to
schedule tasks — scheduling happens directly from Dart.
- Wire the plugin registrant callback in your macOS
AppDelegate.swiftso other plugins are available in the background engine:
import Cocoa
import FlutterMacOS
import workmanager_apple
@main
class AppDelegate: FlutterAppDelegate {
override func applicationDidFinishLaunching(_ notification: Notification) {
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
RegisterGeneratedPlugins(registry: registry)
}
super.applicationDidFinishLaunching(notification)
}
}- Name your dispatcher
callbackDispatcher— see the Basic Usage section below. On macOS the function must be a top-level function in your app's main library (e.g.main.dart) with that exact name.
macOS limitations: Tasks only run while the app is running or backgrounded and the Mac is awake. They do not run after the app is quit. Timing is best-effort — the system may defer activities while the Mac is busy, and network/charging constraints are not supported.
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
switch (task) {
case "com.yourapp.processing_task": // Must match Info.plist and AppDelegate
await syncDataWithServer();
break;
case "com.yourapp.periodic_task": // Must match Info.plist and AppDelegate
await cleanupOldFiles();
break;
case Workmanager.iOSBackgroundTask:
// iOS Background Fetch task
await handleBackgroundFetch();
break;
default:
// Handle unknown task types
break;
}
return Future.value(true);
});
}Important: The callbackDispatcher must be a top-level function (not inside a class) since it runs in a separate isolate.
callbackDispatcher is the entry point of your background isolate. It is not
the task itself: it is the function that iOS and Android start whenever a
scheduled task becomes due, and inside it you tell the plugin which handler to
run via Workmanager().executeTask(...).
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
// This closure is where your background work actually runs.
switch (task) {
case 'data_sync':
await syncData(inputData);
break;
}
return Future.value(true);
});
}Requirements and behavior:
- It must be a top-level function or a static method. Flutter needs to
look it up by handle when the app is started from the background, which is
only possible for top-level/static functions (
PluginUtilities.getCallbackHandlereturnsnullfor anything else). - It must be annotated with
@pragma('vm:entry-point')so the Dart compiler keeps it reachable for background starts. - It runs in a separate isolate from your UI. Initialize plugins and
dependencies (e.g.
DartPluginRegistrant.ensureInitialized(),SharedPreferences,Firebase.initializeApp) inside the handler, not inmain(). - You register exactly one dispatcher per app (in
main()) and dispatch ontaskinside it. You do not need a separatecallbackDispatcherper task.
The name can be confusing: the function is really your task runner, but the
name is kept for historical reasons. Feel free to name it taskRunner or
backgroundTaskHandler in your own code.
import 'package:flutter/foundation.dart';
void main() {
Workmanager().initialize(callbackDispatcher);
runApp(MyApp());
}// Schedule a one-time task
Workmanager().registerOneOffTask(
"sync-task",
"data_sync", // taskName: the value your callback receives (no AppDelegate setup needed)
initialDelay: Duration(seconds: 10),
);
// Schedule a periodic task
Workmanager().registerPeriodicTask(
"cleanup-task",
"com.yourapp.periodic_task", // Must match Info.plist and AppDelegate
frequency: Duration(hours: 24),
);
// Schedule a periodic task with input data
Workmanager().registerPeriodicTask(
"sync-task",
"data_sync",
frequency: Duration(hours: 6),
inputData: <String, dynamic>{
'server_url': 'https://api.example.com',
'sync_type': 'full',
'max_retries': 3,
},
);iOS: registerPeriodicTask requires BGTaskScheduler setup. On iOS the
uniqueName you pass here is submitted to BGTaskScheduler, so it must appear in
BGTaskSchedulerPermittedIdentifiers in Info.plist. The launch handler is
registered automatically by the plugin (at schedule time and on the next app
launch), so no AppDelegate code is required. Without the Info.plist entry
you'll get BGTaskSchedulerErrorDomain Code 3 ("not advertised in the
application's Info.plist"). On Android no native setup is needed.
For one-off work that must start promptly (a user-initiated upload, a
notification the user is waiting on), pass expedited: true:
await Workmanager().registerOneOffTask(
"sync-user-data",
"syncTask",
expedited: true,
);On Android 12+ (API 31+) the system runs expedited work as a WorkManager-
managed foreground service and shows a notification while it runs. Expedited
work is one-off only (periodic tasks cannot be expedited) and is meant for
short, user-visible work — not long-running processing. Other platforms ignore
the flag. See
Customization → Expedited work for
details and the outOfQuotaPolicy interplay.
Android's WorkManager runs background workers for a limited time (typically a few minutes). For work that legitimately takes longer — bulk uploads or downloads, ML processing, large file operations — Android's first-class answer is a foreground service: the worker is promoted to the foreground, the process is kept alive, and a notification stays visible for the whole duration of the task.
To run a task as a foreground service, pass a foregroundServiceConfig when
registering the task:
await Workmanager().registerOneOffTask(
"upload-task",
"upload_files",
inputData: <String, dynamic>{
'destination': 'https://api.example.com/uploads',
},
foregroundServiceConfig: ForegroundServiceConfig(
notificationTitle: "Uploading files",
notificationText: "Your files are being uploaded",
),
);The same option is available on registerPeriodicTask. The notification is
shown as soon as the task starts and is removed automatically when the task
finishes (or is cancelled). The worker keeps running even if the app is in the
background or closed, for as long as the task takes.
ForegroundServiceConfig has sane defaults for every field; you only need to
provide the notification text you want to show:
| Field | Default |
|---|---|
notificationTitle | Task in progress |
notificationText | Your task is still running |
notificationChannelId | workmanager_foreground_tasks |
notificationChannelName | Long-running tasks |
notificationId | 0 |
foregroundServiceType | ForegroundServiceType.dataSync |
The supported foreground service types are dataSync (default; for
synchronization, uploads and downloads) and shortService (short, critical
work that must complete quickly):
foregroundServiceConfig: ForegroundServiceConfig(
notificationTitle: "Cleanup",
notificationText: "Removing temporary files",
foregroundServiceType: ForegroundServiceType.shortService,
),Android 13+ notification permission: on Android 13 (API 33) and newer the
foreground service still runs, but the notification is only visible when the
app holds the POST_NOTIFICATIONS runtime permission. Request it from your UI
before registering the task if you want the notification to be shown.
Android 14+ foreground service types: apps targeting SDK 34+ must declare
the foreground service type in the manifest and hold the matching permission.
The plugin already declares dataSync and shortService (and their
permissions) for you — no native setup is required.
Android 15+ limitations: on Android 15 (API 35), dataSync foreground
services may run for at most 6 hours in any 24-hour period, and work launched
directly from BOOT_COMPLETED may be blocked from starting a dataSync or
shortService foreground service by the system. In that case the task simply
falls back to running as a regular background worker within the usual limits.
Your background tasks can return:
Future.value(true)- ✅ Task successfulFuture.value(false)- 🔄 Task should be retriedFuture.error(...)- ❌ Task failed
- Callback Dispatcher: Must be a top-level function (not inside a class)
- Separate Isolate: Background tasks run in isolation - initialize dependencies inside the task
- iOS Task Identifiers: When using BGTaskScheduler (Options B & C), task names in Dart must exactly match the identifiers in
BGTaskSchedulerPermittedIdentifiersin Info.plist - Platform Differences:
- Android: Reliable background execution, 15-minute minimum frequency
- iOS: 30-second limit, execution depends on user patterns and device state
- Task Customization - Advanced configuration with constraints, input data, and management
- Debugging Guide - Learn how to debug and troubleshoot background tasks
- Example App - Complete working demo