close

Quick Start

Get started with Flutter Workmanager in minutes

Installation

Add workmanager to your pubspec.yaml:

yaml
dependencies:
  workmanager: ^0.10.0

Then run:

bash
flutter pub get

Platform Setup

Android

Android works automatically - no additional setup required! ✅

iOS

iOS requires a 5-minute setup in Xcode. Choose your approach based on your needs:

Use other Flutter plugins inside background tasks (iOS)

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:

swift
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)
  }
}

Option A: Periodic Tasks (Recommended for most use cases)

For regular data sync, notifications, cleanup - uses iOS Background Fetch:

  1. Enable Background Modes in Xcode target capabilities (Configuration Guide) and add to Info.plist (UIBackgroundModes reference):
xml
<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
</array>
  1. No AppDelegate configuration needed - works automatically from Dart code

Option B: Processing Tasks (For complex operations)

For file uploads, data processing, longer tasks - uses BGTaskScheduler:

  1. Enable Background Modes in Xcode target capabilities (Configuration Guide) and add to Info.plist (UIBackgroundModes reference):
xml
<key>UIBackgroundModes</key>
<array>
    <string>processing</string>
</array>

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.yourapp.processing_task</string>
</array>
  1. Configure AppDelegate.swift (required for BGTaskScheduler):
swift
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.

Option C: Periodic Tasks with Custom Frequency

For periodic tasks with more control than Background Fetch - uses BGTaskScheduler with frequency:

  1. Enable Background Modes in Xcode target capabilities (Configuration Guide) and add to Info.plist (UIBackgroundModes reference):
xml
<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
</array>

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.yourapp.periodic_task</string>
</array>
  1. (Optional) Configure AppDelegate.swift for custom frequency control:
swift
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.

Option D: Health Research Tasks (iOS 17+)

For apps participating in a Health Research Study — BGHealthResearchTaskRequest gets additional priority/reliability for study-essential processing:

  1. 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.healthresearch entitlement must be present in your .entitlements file.
    • The user must have opted in to the study.
  2. Enable Background Modes and add the identifier to Info.plist:

xml
<key>UIBackgroundModes</key>
<array>
    <string>processing</string>
</array>

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.yourapp.health_research_task</string>
</array>
  1. (Optional) Pre-register the launch handler in AppDelegate.swift:
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.

  1. Schedule from Dart (iOS 17+; older iOS versions receive an error):
dart
await Workmanager().registerHealthResearchTask(
  'com.yourapp.health_research_task',
  'healthResearchTask',
  initialDelay: const Duration(hours: 1),
  constraints: Constraints(
    networkType: NetworkType.connected,
    requiresCharging: true,
  ),
);

Option E: Continued Processing Tasks (iOS 26+)

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.

  1. 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:
xml
<key>UIBackgroundModes</key>
<array>
    <string>processing</string>
</array>

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.yourapp.continuedProcessing.*</string>
</array>
  1. (Optional) Pre-register the launch handler in AppDelegate.swift:
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.

  1. Schedule from Dart (iOS 26+; older iOS versions receive an error):
dart
await Workmanager().registerContinuedProcessingTask(
  'com.yourapp.continuedProcessing.*',
  'continuedProcessingTask',
  title: 'Example continued processing',
  subtitle: 'Processing in progress',
  inputData: {'frames': 120},
);

macOS

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.

  1. Wire the plugin registrant callback in your macOS AppDelegate.swift so other plugins are available in the background engine:
swift
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)
  }
}
  1. 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.

Basic Usage

1. Create Background Task Handler

dart
@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);
  });
}

What is callbackDispatcher?

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(...).

dart
@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.getCallbackHandle returns null for 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 in main().
  • You register exactly one dispatcher per app (in main()) and dispatch on task inside it. You do not need a separate callbackDispatcher per 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.

2. Initialize in main()

dart
import 'package:flutter/foundation.dart';

void main() {
  Workmanager().initialize(callbackDispatcher);
  
  runApp(MyApp());
}

3. Schedule Tasks

dart
// 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,
  },
);

Expedited tasks (Android only)

For one-off work that must start promptly (a user-initiated upload, a notification the user is waiting on), pass expedited: true:

dart
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.

Long-running tasks (foreground service)

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:

dart
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:

FieldDefault
notificationTitleTask in progress
notificationTextYour task is still running
notificationChannelIdworkmanager_foreground_tasks
notificationChannelNameLong-running tasks
notificationId0
foregroundServiceTypeForegroundServiceType.dataSync

The supported foreground service types are dataSync (default; for synchronization, uploads and downloads) and shortService (short, critical work that must complete quickly):

dart
foregroundServiceConfig: ForegroundServiceConfig(
  notificationTitle: "Cleanup",
  notificationText: "Removing temporary files",
  foregroundServiceType: ForegroundServiceType.shortService,
),

Task Results

Your background tasks can return:

  • Future.value(true) - ✅ Task successful
  • Future.value(false) - 🔄 Task should be retried
  • Future.error(...) - ❌ Task failed

Key Points

  • 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 BGTaskSchedulerPermittedIdentifiers in Info.plist
  • Platform Differences:
    • Android: Reliable background execution, 15-minute minimum frequency
    • iOS: 30-second limit, execution depends on user patterns and device state

Next Steps