Android Foreground Services
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.
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
Servicefrom Dart - Sending task data from the service back to Flutter via an isolate port
- Managing the required foreground notification automatically
- Handling
FOREGROUND_SERVICEandPOST_NOTIFICATIONSpermissions
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>
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
Serviceobject 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 theRECEIVE_BOOT_COMPLETEDpermission and aBroadcastReceiverregistered in the manifest — the package handles this automatically).
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.