Skip to content

Android Headless Mode

Chris Scott edited this page Sep 24, 2026 · 26 revisions

The BackgroundGeolocation SDK provides an Android headless mechanism, enabled with AppConfig.enableHeadless. It lets you provide your own Dart function to receive the SDK's events after your app has been terminated.

When your app is terminated with stopOnTerminate: false, the SDK keeps running natively. With enableHeadless: true, the plugin starts a background Flutter engine and delivers each event to your headless task. Your usual listeners (onLocation, onMotionChange, etc.) went away with your UI, so they receive nothing until the app is launched again.

No Java or Kotlin is needed: the plugin provides the Android side for you.


ℹ️ iOS has no concept of "headless". When iOS launches an app in the background (for a geofence event, for example), it launches the entire app, just as if the user had tapped the home-screen icon.


Step 1: registerHeadlessTask

Open your main.dart file. Create a top-level function to receive headless events, and register it with registerHeadlessTask in main(), after runApp:

⚠️ Do this in main.dart, beside void main(). Do not create or register the headless task in one of your widgets.

📂 main.dart

import 'package:flutter/material.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;

// Receives events from BackgroundGeolocation while your app is terminated.
// The @pragma annotation is required (Flutter >= 3.3.0).
@pragma('vm:entry-point')
void headlessTask(bg.HeadlessEvent headlessEvent) async {
  print('[BackgroundGeolocation HeadlessTask]: $headlessEvent');
  // Implement a 'case' for only those events you're interested in.
  switch (headlessEvent.name) {
    case bg.Event.BOOT:
      bg.State state = headlessEvent.event;
      print('- State: $state');
      break;
    case bg.Event.TERMINATE:
      bg.State state = headlessEvent.event;
      print('- State: $state');
      break;
    case bg.Event.HEARTBEAT:
      bg.HeartbeatEvent event = headlessEvent.event;
      print('- HeartbeatEvent: $event');
      break;
    case bg.Event.LOCATION:
      bg.Location location = headlessEvent.event;
      print('- Location: $location');
      break;
    case bg.Event.MOTIONCHANGE:
      bg.Location location = headlessEvent.event;
      print('- Location: $location');
      break;
    case bg.Event.LOCATIONFILTER:
      bg.LocationFilterEvent event = headlessEvent.event;
      print('- LocationFilterEvent: $event');
      break;
    case bg.Event.GEOFENCE:
      bg.GeofenceEvent geofenceEvent = headlessEvent.event;
      print('- GeofenceEvent: $geofenceEvent');
      break;
    case bg.Event.GEOFENCESCHANGE:
      bg.GeofencesChangeEvent event = headlessEvent.event;
      print('- GeofencesChangeEvent: $event');
      break;
    case bg.Event.SCHEDULE:
      bg.State state = headlessEvent.event;
      print('- State: $state');
      break;
    case bg.Event.ACTIVITYCHANGE:
      bg.ActivityChangeEvent event = headlessEvent.event;
      print('- ActivityChangeEvent: $event');
      break;
    case bg.Event.HTTP:
      bg.HttpEvent response = headlessEvent.event;
      print('- HttpEvent: $response');
      break;
    case bg.Event.PROVIDERCHANGE:
      bg.ProviderChangeEvent event = headlessEvent.event;
      print('- ProviderChangeEvent: $event');
      break;
    case bg.Event.POWERSAVECHANGE:
      bool enabled = headlessEvent.event;
      print('- PowerSaveChange: $enabled');
      break;
    case bg.Event.CONNECTIVITYCHANGE:
      bg.ConnectivityChangeEvent event = headlessEvent.event;
      print('- ConnectivityChangeEvent: $event');
      break;
    case bg.Event.ENABLEDCHANGE:
      bool enabled = headlessEvent.event;
      print('- EnabledChange: $enabled');
      break;
    case bg.Event.AUTHORIZATION:
      bg.AuthorizationEvent event = headlessEvent.event;
      print('- AuthorizationEvent: $event');
      break;
    case bg.Event.NOTIFICATIONACTION:
      String buttonId = headlessEvent.event;
      print('- NotificationAction: $buttonId');
      break;
  }
}

void main() {
  runApp(const MyApp());  // <-- your app's root widget

  // Register your headless task:
  bg.BackgroundGeolocation.registerHeadlessTask(headlessTask);
}

Cast each HeadlessEvent.event according to HeadlessEvent.name, as the switch above does. It is the same class the matching event listener receives (for example, onLocation receives a bg.Location). boot and terminate have no listener: they provide a bg.State. Within your headless task, you're free to call the bg.BackgroundGeolocation API.


⚠️ Warning:

  • You cannot register more than one headless task.
  • You cannot reference your UI within your headless task. There is no UI.
  • Annotate your headless task with @pragma('vm:entry-point'). Since Flutter 3.3.0, a function that is looked up from native code, as your headless task is, needs this annotation: without it, release builds can strip the function.
  • Do not register an anonymous function with registerHeadlessTask. The plugin must be able to find your function again from the background engine. That works for a top-level function, but not for an anonymous one, so registerHeadlessTask fails:
void main() {
  runApp(const MyApp());

  // NO!  This will not work.
  bg.BackgroundGeolocation.registerHeadlessTask((bg.HeadlessEvent headlessEvent) {
    print('$headlessEvent');
  });
}
// YES!
@pragma('vm:entry-point')
void myHeadlessTask(bg.HeadlessEvent headlessEvent) async {
  print('$headlessEvent');
}

void main() {
  runApp(const MyApp());

  bg.BackgroundGeolocation.registerHeadlessTask(myHeadlessTask);
}

Step 2: enableHeadless: true

In your application component, where you call bg.BackgroundGeolocation.ready, set enableHeadless: true in bg.AppConfig. Headless events only occur after your app is terminated, so also set stopOnTerminate: false to keep the SDK running when it is. startOnBoot: true resumes tracking after the device reboots.

⚠️ Do not move registerHeadlessTask into your application components: call it from main() (Step 1), which runs every time your app launches.

import 'package:flutter/material.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;

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

  @override
  State<HelloWorldPage> createState() => _HelloWorldPageState();
}

class _HelloWorldPageState extends State<HelloWorldPage> {
  @override
  void initState() {
    super.initState();
    _initPlatformState();
  }

  Future<void> _initPlatformState() async {
    bg.BackgroundGeolocation.onLocation((bg.Location location) {
      print('[onLocation] $location');
    });

    await bg.BackgroundGeolocation.ready(bg.Config(
      app: bg.AppConfig(
        enableHeadless: true,
        stopOnTerminate: false,
        startOnBoot: true,
      ),
    ));
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold(body: Center(child: Text('Hello World')));
  }
}

Testing

  1. Run your app on an Android device, and start tracking with bg.BackgroundGeolocation.start().
  2. Watch the log: adb logcat -s TSLocationManager flutter
  3. Terminate your app by swiping it away from the recent-apps list. (Don't use Force stop from the app's settings: Android then stops the SDK too.)
  4. For each event, the plugin logs 💀 [HeadlessTask <event>] with the tag TSLocationManager, and your headless task's print output appears with the tag flutter. Shortly after Android destroys your app's activity, the SDK sends a terminate event. Move the device, or simulate a location, to see more.

Clone this wiki locally