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.