Push Notifications & Background Services

Android Foreground Services

16 min Lesson 10 of 11

Android Foreground Services

A foreground service is an Android component that performs operations noticeable to the user and must display a persistent notification in the status bar. Unlike background services, foreground services have a higher priority and are far less likely to be killed by the system when memory is low. They are the correct tool for tasks that must continue running even when the user navigates away from your app — such as audio playback, GPS location tracking, file uploads, or fitness workout timers.

Note: Since Android 8.0 (API 26), all background service execution is heavily restricted. If you need long-running work that the user is aware of, a foreground service with a visible notification is the required approach. Silent background services started with startService() are throttled or killed.

How Flutter Communicates with Android Services

Flutter itself runs in a Dart isolate on top of the Android runtime. Native Android services are written in Java or Kotlin and live entirely outside the Dart layer. Flutter communicates with them through Platform Channels — specifically MethodChannel for one-shot calls and EventChannel for streams. The recommended Flutter package that wraps this plumbing for foreground services is flutter_foreground_task. It handles:

  • Starting and stopping the native Service from Dart
  • Sending task data from the service back to Flutter via an isolate port
  • Managing the required foreground notification automatically
  • Handling FOREGROUND_SERVICE and POST_NOTIFICATIONS permissions

Required AndroidManifest Permissions

Before any code runs, you must declare the correct permissions and service entry in android/app/src/main/AndroidManifest.xml:

AndroidManifest.xml additions

<!-- Required permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application ...>
  <!-- Register the foreground service component -->
  <service
    android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
    android:foregroundServiceType="location"
    android:stopWithTask="false" />
</application>
Warning: Android 14+ requires the foregroundServiceType attribute to match the actual work being done (e.g. location, mediaPlayback, dataSync). Mismatching types causes a ForegroundServiceStartNotAllowedException at runtime.

Initialising the Foreground Task in Dart

The flutter_foreground_task package exposes a clean Dart API. The first step is to call FlutterForegroundTask.init() once — typically in main() or in initState() before starting the service. You pass it an AndroidNotificationOptions object that describes the persistent notification the user will see.

Initialising and starting a foreground service

import 'package:flutter_foreground_task/flutter_foreground_task.dart';

// 1. Initialise once (e.g. in main or a dedicated setup method)
void initForegroundTask() {
  FlutterForegroundTask.init(
    androidNotificationOptions: AndroidNotificationOptions(
      channelId: 'location_tracking_channel',
      channelName: 'Location Tracking',
      channelDescription: 'Tracks your position in the background.',
      channelImportance: NotificationChannelImportance.LOW,
      priority: NotificationPriority.LOW,
      iconData: const NotificationIconData(
        resType: ResourceType.mipmap,
        resPrefix: ResourcePrefix.ic,
        name: 'launcher',
      ),
    ),
    iosNotificationOptions: const IOSNotificationOptions(
      showNotification: true,
      playSound: false,
    ),
    foregroundTaskOptions: const ForegroundTaskOptions(
      interval: 5000,          // callback fires every 5 s
      isOnceEvent: false,
      autoRunOnBoot: true,     // restart after device reboot
      allowWakeLock: true,
      allowWifiLock: true,
    ),
  );
}

// 2. Start the service, passing a top-level task handler function
Future<bool> startForegroundTask() async {
  if (await FlutterForegroundTask.isRunningService) {
    return FlutterForegroundTask.restartService();
  }
  return FlutterForegroundTask.startService(
    notificationTitle: 'Location Tracking Active',
    notificationText: 'Tap to return to the app',
    callback: startCallback,   // must be a top-level function
  );
}

Writing the Task Handler

The callback runs inside a separate Dart isolate spawned by the native service. It must be a top-level function (not a method or closure) so the isolate can locate it. You implement a class that extends TaskHandler and override three lifecycle methods:

TaskHandler implementation

// Top-level entry point — required by the isolate mechanism
@pragma('vm:entry-point')
void startCallback() {
  FlutterForegroundTask.setTaskHandler(LocationTaskHandler());
}

class LocationTaskHandler extends TaskHandler {
  int _elapsedSeconds = 0;

  // Called once when the isolate starts
  @override
  Future<void> onStart(DateTime timestamp, SendPort? sendPort) async {
    // Initialise resources: open DB, acquire sensor streams, etc.
    _elapsedSeconds = 0;
  }

  // Called repeatedly on the interval defined in ForegroundTaskOptions
  @override
  Future<void> onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {
    _elapsedSeconds += 5;

    // Update the persistent notification text
    FlutterForegroundTask.updateService(
      notificationTitle: 'Tracking: ${_elapsedSeconds}s elapsed',
      notificationText: 'Lat: 24.68  Lng: 46.72',
    );

    // Send data back to the Flutter UI isolate
    sendPort?.send({'elapsed': _elapsedSeconds});
  }

  // Called when the service is about to stop
  @override
  Future<void> onDestroy(DateTime timestamp, SendPort? sendPort) async {
    // Release resources: close streams, flush caches, etc.
  }

  // Called when the user taps the notification action button (if configured)
  @override
  void onButtonPressed(String id) {
    if (id == 'stop') {
      FlutterForegroundTask.stopService();
    }
  }
}

Receiving Data in the Flutter UI

To display live updates from the service in your widget tree, wrap your top-level widget with WithForegroundTask (which handles lifecycle bindings) and use a ReceivePort to listen for messages sent by sendPort?.send(...) inside the handler.

Listening to service data in a widget

class TrackingScreen extends StatefulWidget {
  const TrackingScreen({super.key});

  @override
  State<TrackingScreen> createState() => _TrackingScreenState();
}

class _TrackingScreenState extends State<TrackingScreen> {
  int _elapsed = 0;

  @override
  void initState() {
    super.initState();
    // Register a port to receive messages from the service isolate
    FlutterForegroundTask.addTaskDataCallback(_onReceiveData);
  }

  @override
  void dispose() {
    FlutterForegroundTask.removeTaskDataCallback(_onReceiveData);
    super.dispose();
  }

  void _onReceiveData(Object data) {
    if (data is Map<String, dynamic>) {
      setState(() => _elapsed = data['elapsed'] as int? ?? 0);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Location Tracking')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Elapsed: ${_elapsed}s',
                style: Theme.of(context).textTheme.headlineMedium),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: startForegroundTask,
              child: const Text('Start Service'),
            ),
            ElevatedButton(
              onPressed: FlutterForegroundTask.stopService,
              child: const Text('Stop Service'),
            ),
          ],
        ),
      ),
    );
  }
}

Service Lifecycle

Understanding the foreground service lifecycle prevents resource leaks and unexpected behaviour:

  • onCreate — the Android Service object is created; the Dart isolate is spawned; onStart() is called once.
  • onRepeatEvent — fires on the configured interval while the service is alive.
  • onDestroy — triggered when stopService() is called or the system terminates the service; release all resources here.
  • autoRunOnBoot — if true, the service restarts after device reboot (requires the RECEIVE_BOOT_COMPLETED permission and a BroadcastReceiver registered in the manifest — the package handles this automatically).
Tip: Always call FlutterForegroundTask.stopService() in your app's logout or session-end flow. If you only call it from the UI, a user who force-closes the app without logging out will leave the service running indefinitely — draining the battery and persisting the notification.

Summary

Android foreground services let your Flutter app perform long-running work — location tracking, audio streaming, uploads — while the app is not visible. The key points are: declare the service and correct foregroundServiceType in the manifest, display a persistent notification (mandatory), implement a TaskHandler in a top-level function that runs in its own Dart isolate, and communicate back to the UI via a SendPort. Always release resources in onDestroy and stop the service explicitly when the work is done.