# Background Recorder with Foreground Service (Android only)

Last updated:

<!-- -->

October 18, 2026

<!-- -->

\| 6 minutes read

<br />

<br />

This example demonstrates how to build a Flutter app using the Maps SDK to record a user’s track while running in the **background**.<br /><!-- -->The app integrates with the **Android foreground service** and **notifications API** to keep recording active even when the app is not visible.

## How it works[​](#how-it-works "Direct link to How it works")

The example app highlights the following features:

* Requesting **location** and **notification** permissions.
* Initializing a **foreground service** to enable background location updates.
* Recording a track and saving it to device storage.
* Displaying the recorded track and duration on the map.

![Initial map](/docs/flutter/assets/images/example_flutter_recorder_1-31378054350ed0a93564a592ca1599ab.png)

**Initial map**

![Recording in background](/docs/flutter/assets/images/example_flutter_recorder_3-c84f11394c1d94da0cefc9c3f2e74b02.png)

**Recording in background**

![Recorded track on map](/docs/flutter/assets/images/example_flutter_recorder_2-0df9b412fdced232724cdcf83b981abf.png)

**Recorded track on map**

### UI and Map Integration[​](#ui-and-map-integration "Direct link to UI and Map Integration")

The following code builds the UI with a `GemMap` widget and app bar controls for recording and following the user’s position.

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/recorder_in_background/lib/main.dart?ref_type=heads#L35)

```dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late GemMapController _mapController;
  late Recorder _recorder;

  PermissionStatus _locationPermissionStatus = PermissionStatus.denied;
  PermissionStatus _notificationPermissionStatus = PermissionStatus.denied;

  bool _hasLiveDataSource = false;
  bool _isInitialized = false;
  bool _isRecording = false;
  bool _isRecorder = false;

  @override
  void dispose() {
    GemKit.release();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.deepPurple[900],
        title: const Text(
          'Background Location',
          style: TextStyle(color: Colors.white),
        ),
        actions: [
          if (_hasLiveDataSource && _isRecording == false)
            IconButton(
              onPressed: _startRecording,
              icon: Icon(Icons.radio_button_on, color: Colors.white),
            ),
          if (_isRecording)
            IconButton(
              onPressed: _onStopRecordingButtonPressed,
              icon: Icon(Icons.stop_circle, color: Colors.white),
            ),
          IconButton(
            onPressed: _onFollowPositionButtonPressed,
            icon: const Icon(
              Icons.location_searching_sharp,
              color: Colors.white,
            ),
          ),
        ],
      ),
      body: Stack(
        children: [
          GemMap(
            key: ValueKey("GemMap"),
            onMapCreated: (controller) => _onMapCreated(controller),
            appAuthorization: projectApiToken,
          ),
        ],
      ),
    );
  }

```

### Foreground Service Setup[​](#foreground-service-setup "Direct link to Foreground Service Setup")

The AndroidForegroundService class wraps the flutter\_background\_service and flutter\_local\_notifications plugins. It initializes the notification channel, requests permissions, and manages the lifecycle of the foreground service.

foreground\_service.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/recorder_in_background/lib/foreground_service.dart?ref_type=heads#L11)

```dart
@pragma('vm:entry-point')
class AndroidForegroundService {
  static final service = FlutterBackgroundService();
  static final notificationsPlugin = FlutterLocalNotificationsPlugin();
  static final notificationId = 888;
  static bool hasGrantedNotificationsPermission = false;
  static late final AndroidNotificationChannel channel;

  @pragma('vm:entry-point')
  static Future<void> initialize(bool isForegroundMode) async {
    if (!Platform.isAndroid) return;

    const initSettingsAndroid = AndroidInitializationSettings(
      '@mipmap/ic_launcher',
    );

    const initSettings = InitializationSettings(android: initSettingsAndroid);

    await notificationsPlugin.initialize(initSettings);

    channel = AndroidNotificationChannel(
      notificationId.toString(),
      'MY FOREGROUND SERVICE',
      description: 'This channel is used for background location.',
      importance: Importance.low,
    );

    hasGrantedNotificationsPermission = await notificationsPlugin
            .resolvePlatformSpecificImplementation<
                AndroidFlutterLocalNotificationsPlugin>()
            ?.requestNotificationsPermission() ??
        false;

    if (!hasGrantedNotificationsPermission) {
      return;
    }

    await notificationsPlugin
        .resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(channel);

    await service.configure(
      iosConfiguration: IosConfiguration(),
      androidConfiguration: AndroidConfiguration(
        onStart: onStart,
        autoStart: false,
        autoStartOnBoot: false,
        isForegroundMode: isForegroundMode,
        notificationChannelId: notificationId.toString(),
        foregroundServiceNotificationId: notificationId,
        initialNotificationTitle: 'Background location',
        initialNotificationContent: 'Background location is active',
        foregroundServiceTypes: [AndroidForegroundType.location],
      ),
    );
  }

  @pragma('vm:entry-point')
  static Future<bool> hasGrantedPermission() async {
    if (!Platform.isAndroid) return false;

    return hasGrantedNotificationsPermission = await notificationsPlugin
            .resolvePlatformSpecificImplementation<
                AndroidFlutterLocalNotificationsPlugin>()
            ?.areNotificationsEnabled() ??
        false;
  }

  @pragma('vm:entry-point')
  static Future<bool> start() async {
    if (!Platform.isAndroid) return false;
    final service = FlutterBackgroundService();
    return await service.startService();
  }

  @pragma('vm:entry-point')
  static Future<void> stop() async {
    if (!Platform.isAndroid) return;
    service.invoke("stopService");

    await notificationsPlugin.cancelAll();
  }

```

### Requesting Permissions[​](#requesting-permissions "Direct link to Requesting Permissions")

The app requests both notification and background location permissions before enabling recording and the foreground service.

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/recorder_in_background/lib/main.dart?ref_type=heads#L107)

```dart
  Future<void> _onFollowPositionButtonPressed() async {
    // Ask for notifications permission too
    if (_notificationPermissionStatus != PermissionStatus.granted) {
      _notificationPermissionStatus = await Permission.notification.request();
    }

    if (kIsWeb) {
      final locationPermissionWeb =
          await PositionService.requestLocationPermission();
      _locationPermissionStatus = locationPermissionWeb == true
          ? PermissionStatus.granted
          : PermissionStatus.denied;
    } else {
      // Request WhenInUse permission first
      final whenInUseStatus = await Permission.locationWhenInUse.request();

      if (whenInUseStatus == PermissionStatus.granted) {
        // Then request Always (background) permission
        final backgroundStatus = await Permission.locationAlways.request();
        _locationPermissionStatus = backgroundStatus;
      } else {
        _locationPermissionStatus = whenInUseStatus; // denied or restricted
      }
    }

    if (_locationPermissionStatus == PermissionStatus.granted) {
      if (!_hasLiveDataSource) {
        PositionService.setLiveDataSource();
        _hasLiveDataSource = true;
      }

      final animation = GemAnimation(type: AnimationType.linear);
      _mapController.startFollowingPosition(animation: animation);

      setState(() {});
    }
  }

```

### Starting and Stopping Recording[​](#starting-and-stopping-recording "Direct link to Starting and Stopping Recording")

Recording only starts after the foreground service is initialized. When stopped, the service is shut down and the recorded track is presented.

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/recorder_in_background/lib/main.dart?ref_type=heads#L180)

```dart
  Future<void> _startRecording() async {
    if (!_isInitialized) {
      await _initializeForegroundService();
    }

    AndroidForegroundService.start();

    if (!_isRecorder) {
      await _createRecorder();
    }

    await _recorder.startRecording();

    // Clear displayed paths
    _mapController.preferences.paths.clear();
    _mapController.deactivateAllHighlights();

    setState(() {
      _isRecording = true;
    });
  }

  Future<void> _onStopRecordingButtonPressed() async {
    final endErr = await _recorder.stopRecording();

    if (endErr == GemError.success) {
      await _presentRecordedRoute();
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Recording failed: $endErr'),
            duration: Duration(seconds: 5),
          ),
        );
      }
    }

    AndroidForegroundService.stop();

    setState(() {
      _isRecording = false;
    });
  }

```

### Presenting the Recorded Track[​](#presenting-the-recorded-track "Direct link to Presenting the Recorded Track")

When recording is complete, the last session is loaded from disk and displayed on the map.

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/recorder_in_background/lib/main.dart?ref_type=heads#L225)

```dart
  Future<void> _presentRecordedRoute() async {
    final logsDir = await getDirectoryPath("Tracks");

    // It loads all .gm and .mp4 files at logsDir
    final bookmarks = RecorderBookmarks.create(logsDir);

    // Get all recordings path
    final logList = bookmarks?.getLogsList();

    // Get the LogMetadata to obtain details about recorded session
    LogMetadata? meta = bookmarks!.getLogMetadata(logList!.last);
    if (meta == null) {
      // Handle the case where metadata is not found
      return;
    }
    final recorderCoordinates = meta.preciseRoute;
    final duration = convertDuration(meta.durationMillis);

    if (recorderCoordinates.isEmpty) {
      // ignore: use_build_context_synchronously
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('No recorded coordinates.'),
          duration: Duration(seconds: 5),
        ),
      );
    }

    // Create a path entity from coordinates
    final path = Path.fromCoordinates(recorderCoordinates);

    Landmark beginLandmark = Landmark.withCoordinates(
      recorderCoordinates.first,
    );
    Landmark endLandmark = Landmark.withCoordinates(recorderCoordinates.last);

    beginLandmark.setImageFromIcon(GemIcon.waypointStart);
    endLandmark.setImageFromIcon(GemIcon.waypointFinish);

    HighlightRenderSettings renderSettings = HighlightRenderSettings(
      options: {HighlightOptions.showLandmark},
    );

    _mapController.activateHighlight(
      [beginLandmark, endLandmark],
      renderSettings: renderSettings,
      highlightId: 1,
    );

    // Show the path immediately after stopping recording
    _mapController.preferences.paths.add(path);

    // Center on recorder path
    _mapController.centerOnAreaRect(
      path.area,
      viewRc: Rectangle(
        _mapController.viewport.width ~/ 3,
        _mapController.viewport.height ~/ 3,
        _mapController.viewport.width ~/ 3,
        _mapController.viewport.height ~/ 3,
      ),
    );

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Duration: $duration'),
          duration: Duration(seconds: 5),
        ),
      );
    }
  }

```

### Utility Functions[​](#utility-functions "Direct link to Utility Functions")

utils.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/recorder_in_background/lib/utils.dart?ref_type=heads#L11)

```dart
Future<String> getDirectoryPath(String dirName) async {
  final docDirectory = Platform.isAndroid
      ? await path_provider.getExternalStorageDirectory()
      : await path_provider.getApplicationDocumentsDirectory();

  String absPath = docDirectory!.path;

  final expectedPath = path.joinAll([absPath, "Data", dirName]);
  return expectedPath;
}

// Utility function to convert the seconds duration into a suitable format
String convertDuration(int milliseconds) {
  int totalSeconds = milliseconds ~/ 1000;
  int hours = totalSeconds ~/ 3600;
  int minutes = (totalSeconds % 3600) ~/ 60;
  int remainingSeconds = totalSeconds % 60;

  String hoursText = (hours > 0) ? '$hours h ' : '';
  String minutesText = (minutes > 0) ? '$minutes min ' : '';
  String secondsText =
      (hours == 0 && minutes == 0) ? '$remainingSeconds sec' : '';

  return (hoursText + minutesText + secondsText).trim();
}

```

Required Permissions

To ensure this example functions correctly, the necessary permissions must be added to the project's Android and iOS configuration files:

* Android

Add the following code to the `android/app/src/main/AndroidManifest.xml` file, within the `<manifest>` block:

```
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<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" />
```

This example uses the<!-- --> [Permission Handler](https://pub.dev/packages/permission_handler) <!-- -->package. Be sure to follow the<!-- --> [setup guide](https://pub.dev/packages/permission_handler#setup).
