Implementing Background Location Tracking in Flutter

Location tracking is a vital feature in many mobile applications, ranging from navigation apps to fitness trackers. When you need location updates even when the app is not in the foreground, you’ll require background location tracking. Implementing background location tracking in Flutter can be complex, especially with platform-specific configurations for Android and iOS. This article walks you through implementing background location tracking in Flutter, providing detailed code samples and explanations.

What is Background Location Tracking?

Background location tracking allows your app to receive location updates even when the app is minimized, suspended, or the screen is turned off. This functionality is essential for use cases like geofencing, fitness tracking, and real-time location sharing. However, it also raises privacy concerns, making it important to handle it responsibly.

Why is Background Location Tracking Important?

  • Enhanced Functionality: Enables features that require continuous location updates.
  • Improved User Experience: Delivers seamless and reliable service regardless of the app’s state.
  • Versatile Applications: Supports a wide range of use cases from navigation to health monitoring.

Prerequisites

Before you start, ensure you have:

  • Flutter SDK installed.
  • Android Studio and/or Xcode set up for building native code.
  • Basic understanding of Flutter development.

Step-by-Step Implementation

Step 1: Create a New Flutter Project

First, create a new Flutter project using the following command:

flutter create background_location_app
cd background_location_app

Step 2: Add Dependencies

Add the necessary dependencies to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  geolocator: ^9.0.2
  background_locator_2: ^2.0.1+3
  permission_handler: ^10.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

Run flutter pub get to install these dependencies.

Step 3: Configure Android

Android Permissions

Open the android/app/src/main/AndroidManifest.xml file and add the following permissions:




    
    
    
    

    

        <!-- Add the service here -->
        <service
            android:name="com.transistorsoft.flutter.backgroundgeolocation.BackgroundGeolocation"
            android:foregroundServiceType="location"
            android:exported="false">
        </service>
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
        ...
    

Background Execution

In MainActivity.kt or MainActivity.java, ensure you have the following code to handle the background execution correctly:


import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
    }
}

Step 4: Configure iOS

iOS Permissions

Open the ios/Runner/Info.plist file and add the following keys to request location permissions:


<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location when open to provide location-based services.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to your location in the background to provide location-based services.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide location-based services.</string>
<key>UIBackgroundModes</key>
<array>
 <string>location</string>
</array>

Step 5: Implementing the Flutter Code

Now, implement the Dart code to handle location tracking.

Import Packages

Import the necessary packages in your main.dart file:


import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:background_locator_2/background_locator.dart';
import 'package:background_locator_2/location_dto.dart';
import 'package:background_locator_2/settings/android_settings.dart';
import 'package:background_locator_2/settings/ios_settings.dart';
import 'package:permission_handler/permission_handler.dart';
Define Variables

Define the variables required for tracking:


ReceivePort port = ReceivePort();

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

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String logStr = '';
  bool isRunning = false;

  @override
  void initState() {
    super.initState();
    if (IsolateNameServer.lookupPortByName('locator_port') != null) {
      IsolateNameServer.removePortNameMapping('locator_port');
    }
    IsolateNameServer.registerPortWithName(port.sendPort, 'locator_port');
    port.listen((dynamic data) {
      setState(() {
        logStr = '$data';
      });
    });
    initPlatformState();
  }

  Future<void> initPlatformState() async {
    await BackgroundLocator.initialize();
  }

  @override
  Widget build(BuildContext context) {
    const textStyle = TextStyle(fontSize: 16);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Background Location Tracking'),
        ),
        body: Container(
          padding: const EdgeInsets.all(22),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              ElevatedButton(
                onPressed: () async {
                  if (isRunning) {
                    return;
                  }
                  _onStart();
                },
                child: const Text('Start Tracking'),
              ),
              ElevatedButton(
                onPressed: () {
                  if (!isRunning) {
                    return;
                  }
                  _onStop();
                },
                child: const Text('Stop Tracking'),
              ),
              const SizedBox(height: 10),
              Text('Logs:', style: textStyle),
              const SizedBox(height: 10),
              Text(logStr, style: textStyle),
            ],
          ),
        ),
      ),
    );
  }

  void _onStart() async {
    if (await _checkLocationPermission()) {
      setState(() {
        isRunning = true;
        logStr = 'Tracking Started';
      });

      await BackgroundLocator.registerLocationUpdate(
        callback: locationCallback,
        initCallback: initCallback,
        initDataCallback: initDataCallback,
        androidSettings: AndroidSettings(
            notificationChannelName: 'LocationTracking',
            notificationTitle: 'Start Location Tracking',
            notificationMsg: 'Location tracking in background started',
            wakeLockTime: 20,
            callbackCollectData: true,
            autoCancel: false),
        iosSettings: IOSSettings(
          accuracy: LocationAccuracy.best,
          distanceFilter: 0,
          stopTimeout: 1,
        ),
      );
    } else {
      setState(() {
        logStr = 'Location permission not granted';
      });
    }
  }

  void _onStop() async {
    setState(() {
      isRunning = false;
      logStr = 'Tracking Stopped';
    });
    await BackgroundLocator.unRegisterLocationUpdate();
  }

  Future<bool> _checkLocationPermission() async {
    var status = await Permission.location.status;
    if (status.isGranted) {
      return true;
    } else {
      var result = await Permission.location.request();
      return result == PermissionStatus.granted;
    }
  }
}

@pragma('vm:entry-point')
void initCallback(DateTime time) {
  print('=====Init callback : $time');
}

@pragma('vm:entry-point')
void initDataCallback(DateTime time) {
  print('=====Init data callback : $time');
}

@pragma('vm:entry-point')
void locationCallback(LocationDto location) {
  final SendPort? send = IsolateNameServer.lookupPortByName('locator_port');
  send?.send('${DateTime.now()}: lat ${location.latitude}, long ${location.longitude}');
}

Step 6: Add Background Locator Initialization

Configure the background locator to run when the app starts.

Define Callback Functions

Define the callback functions for handling location updates:


@pragma('vm:entry-point')
void initCallback(DateTime time) {
  print('=====Init callback : $time');
}

@pragma('vm:entry-point')
void initDataCallback(DateTime time) {
  print('=====Init data callback : $time');
}

@pragma('vm:entry-point')
void locationCallback(LocationDto location) {
  final SendPort? send = IsolateNameServer.lookupPortByName('locator_port');
  send?.send('${DateTime.now()}: lat ${location.latitude}, long ${location.longitude}');
}
Register Location Updates

Register location updates in the _onStart method:


Future<void> _onStart() async {
    if (await _checkLocationPermission()) {
      setState(() {
        isRunning = true;
        logStr = 'Tracking Started';
      });

      await BackgroundLocator.registerLocationUpdate(
        callback: locationCallback,
        initCallback: initCallback,
        initDataCallback: initDataCallback,
        androidSettings: AndroidSettings(
            notificationChannelName: 'LocationTracking',
            notificationTitle: 'Start Location Tracking',
            notificationMsg: 'Location tracking in background started',
            wakeLockTime: 20,
            callbackCollectData: true,
            autoCancel: false),
        iosSettings: IOSSettings(
          accuracy: LocationAccuracy.best,
          distanceFilter: 0,
          stopTimeout: 1,
        ),
      );
    } else {
      setState(() {
        logStr = 'Location permission not granted';
      });
    }
  }
Unregister Location Updates

Unregister location updates in the _onStop method:


void _onStop() async {
    setState(() {
      isRunning = false;
      logStr = 'Tracking Stopped';
    });
    await BackgroundLocator.unRegisterLocationUpdate();
  }

Testing the Implementation

Run your app on both Android and iOS devices. Ensure you grant the necessary permissions and verify that location updates are received even when the app is in the background.

Android Testing

For Android, you can use Android Studio’s emulator or a physical device. Ensure that the location services are enabled and that the app has the necessary permissions.

iOS Testing

For iOS, you can use Xcode’s simulator or a physical device. Grant the app location permissions and test background location updates. Note that iOS may aggressively suspend background tasks, so testing on a physical device is recommended.

Handling Edge Cases and Optimizations

  • Battery Optimization: Minimize battery usage by adjusting the frequency of location updates.
  • Error Handling: Implement robust error handling to manage scenarios where location services are unavailable.
  • User Privacy: Always request and respect user consent for location tracking.

Conclusion

Implementing background location tracking in Flutter requires careful attention to both platform-specific configurations and Flutter code. By following the steps outlined in this article and considering the edge cases, you can create reliable and efficient location-aware applications.