Skip to main content

Handling Background Tasks in Flutter: WorkManager vs. flutter_background_service

 Mobile operating systems are hostile environments for background execution. If your Flutter app attempts to fetch location updates, sync a database, or maintain a WebSocket connection while the user has the app minimized (or worse, swiped away), iOS and Android will relentlessly kill the process to preserve battery life.

The concept of "keeping the app alive" does not exist natively. Instead, you must negotiate with the OS for execution windows. Choosing the wrong tool—specifically between workmanager and flutter_background_service—leads to silent failures, negative App Store reviews, and inconsistent behavior across devices.

The Root Cause: Doze, Buckets, and The Watchdog

To solve this, we must understand why the OS kills your Dart code.

  1. Android's Doze & App Standby: Since Android 6.0, the system groups apps into "buckets" (Active, Working Set, Frequent, Rare). If your app is not in the foreground, it loses network access and CPU cycles. Android requires a Foreground Service (a persistent notification) to elevate the process priority, signaling to the OS that the user is aware the app is running. Without this, the ActivityManager sends a SIGKILL to your process shortly after the screen turns off.
  2. iOS Execution Suspension: iOS is far stricter. When an app enters the background, it has seconds to finish tasks before the memory snapshot is frozen. To execute code later, you must rely on specific UIBackgroundModes (like Audio, VoIP, or Location) or BGTaskScheduler. You cannot simply spawn a thread and hope it runs.
  3. The Flutter Engine Detachment: Flutter's UI runs in the main Isolate. When the app is terminated, that Isolate dies. To run code in the background, you must spawn a Headless Isolate. This is a separate Dart execution context that runs without a FlutterView. If this entry point isn't registered correctly with @pragma('vm:entry-point'), the Dart VM will tree-shake it out during release builds.

The Strategy: Choosing the Right Tool

Do not look for a "one size fits all" solution. You must categorize your task based on frequency and urgency.

Scenario A: Periodic Data Sync (Deferrable)

Tool: workmanager Use this if you need to upload logs or sync specific data once every 15+ minutes. It uses WorkManager (Android) and BGAppRefreshTaskRequest (iOS). It respects battery life and runs only when the OS decides conditions are optimal (e.g., device charging, on WiFi).

Scenario B: Continuous Execution (Immediate/Exact)

Tool: flutter_background_service Use this for navigation, live location tracking, or real-time socket listeners. This wraps an Android Foreground Service and creates a persistent isolate.

The Fix: Implementing High-Reliability Continuous Tasks

Below is a production-grade implementation using flutter_background_service to maintain a heartbeat even when the app is terminated. This is the only reliable way to handle use cases like "Live Location Tracking."

1. Platform Configuration

Android (android/app/src/main/AndroidManifest.xml): You must declare the service and permissions. Note the foregroundServiceType requirement for Android 14+.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <!-- Required for Android 14+ -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> 
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <application>
        <!-- ... other configs ... -->
        
        <service
            android:name="id.flutter.flutter_background_service.BackgroundService"
            android:foregroundServiceType="dataSync" 
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:enabled="true"
            android:exported="true"
        />
    </application>
</manifest>

iOS (ios/Runner/Info.plist): Enable background fetch and processing. Note: For true continuous execution on iOS, you often need 'Location' or 'Audio' modes. Pure processing tasks are still subject to scheduler throttling.

<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
    <string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.yourcompany.app.taskId</string>
</array>

2. The Dart Implementation

This code sets up a detached isolate. It handles the critical bidirectional communication between your UI and the background runner.

import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

// 1. ANNOTATION IS CRITICAL
// Prevents the Dart compiler from stripping this function during release builds.
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
  // Only available for Flutter 3.0.0 and later
  DartPluginRegistrant.ensureInitialized();

  // Initialize Local Notifications for the Android Foreground Service
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  if (service is AndroidServiceInstance) {
    service.on('setAsForeground').listen((event) {
      service.setAsForegroundService();
    });

    service.on('setAsBackground').listen((event) {
      service.setAsBackgroundService();
    });
  }

  service.on('stopService').listen((event) {
    service.stopSelf();
  });

  // Bring up the Foreground Notification to keep Android happy
  if (service is AndroidServiceInstance) {
    await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(
      const AndroidNotificationChannel(
        'my_foreground',
        'MY_FOREGROUND_SERVICE',
        description: 'This channel is used for important notifications.',
        importance: Importance.low, // Low importance prevents sound/vibration spam
      ),
    );

    await service.setForegroundNotificationInfo(
      title: "App is running",
      content: "Background processing active",
    );
  }

  // CORE LOGIC LOOP
  // This timer runs even when the app is closed.
  Timer.periodic(const Duration(seconds: 15), (timer) async {
    if (service is AndroidServiceInstance) {
      if (await service.isForegroundService()) {
        
        // --- YOUR HEAVY LIFTING CODE HERE ---
        // e.g., Geolocator.getCurrentPosition(), Socket emission, etc.
        print('Background service running: ${DateTime.now()}');
        
        // Update notification content to show liveness
        flutterLocalNotificationsPlugin.show(
          888,
          'Background Service',
          'Last update: ${DateTime.now()}',
          const NotificationDetails(
            android: AndroidNotificationDetails(
              'my_foreground',
              'MY_FOREGROUND_SERVICE',
              icon: 'ic_bg_service_small',
              ongoing: true,
            ),
          ),
        );
        
        // Send data back to the UI (if UI is alive)
        service.invoke(
          'update',
          {
            "current_date": DateTime.now().toIso8601String(),
          },
        );
      }
    }
  });
}

Future<void> initializeService() async {
  final service = FlutterBackgroundService();

  await service.configure(
    androidConfiguration: AndroidConfiguration(
      // This will be executed in the separate Isolate
      onStart: onStart,

      // Auto start service on boot and when app opens
      autoStart: true,
      isForegroundMode: true,
      
      notificationChannelId: 'my_foreground',
      initialNotificationTitle: 'Initializing',
      initialNotificationContent: 'Preparing background service',
      foregroundServiceNotificationId: 888,
    ),
    iosConfiguration: IosConfiguration(
      autoStart: true,
      // This handles the background fetch execution on iOS
      onForeground: onStart, 
      onBackground: onIosBackground,
    ),
  );
}

// iOS Background Handler
@pragma('vm:entry-point')
Future<bool> onIosBackground(ServiceInstance service) async {
  WidgetsFlutterBinding.ensureInitialized();
  DartPluginRegistrant.ensureInitialized();
  
  // iOS grants short windows of execution time.
  // Perform quick syncs here.
  print('FLUTTER BACKGROUND FETCH: ${DateTime.now()}');
  
  return true;
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeService();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: StreamBuilder<Map<String, dynamic>?>(
          stream: FlutterBackgroundService().on('update'),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return const Center(child: Text('Waiting for background data...'));
            }
            final data = snapshot.data!;
            return Center(
              child: Text('Background Time: ${data["current_date"]}'),
            );
          },
        ),
      ),
    );
  }
}

The Explanation

1. The Detached Isolate

In initializeService, we pass the onStart function reference. The plugin spawns a new Flutter Engine instance that is completely decoupled from your UI. This is why onStart must be @pragma('vm:entry-point') and static/global. It cannot access the context of your widgets or the state of your MyApp class.

2. The Android Foreground Notification

The isForegroundMode: true and setForegroundNotificationInfo calls are not cosmetic. They create a persistent notification in the Android status bar. This puts the application into a higher priority bucket. Without this, Android 12+ will categorize the app as a "phantom process" and kill it if it uses excessive CPU, regardless of whether you think it's running.

3. iOS Limitations

The code above separates logic for Android (onStart) and iOS (onIosBackground). On iOS, flutter_background_service leverages BGTaskScheduler. You do not get indefinite execution time on iOS unless you are playing audio, handling VoIP, or actively navigating with GPS. The onIosBackground function expects a boolean return value to signal completion to the OS. If you take too long (usually >30 seconds), iOS will penalize your app.

Conclusion

Background execution is a trade-off between functionality and OS resource management constraints.

  • Use WorkManager if your feature is "sync my data eventually."
  • Use Flutter Background Service with a Foreground Notification if your feature is "track me right now" or "keep this connection open."

Attempting to use WorkManager for live tracking will result in updates that lag by 15 minutes. Attempting to use a Foreground Service for simple data sync will result in an annoying persistent notification that degrades the user experience. Choose the architecture that matches the urgency of your data.