flutter_bg_location 1.0.0
flutter_bg_location: ^1.0.0 copied to clipboard
A Flutter plugin for background location tracking that continues even when the app is killed. Features include persistent storage, real-time updates, and server sync.
Flutter Background Location Plugin #
A Flutter plugin for tracking device location in the background, even when the app is killed. Works on both Android and iOS with persistent location storage and optional server sync.
Features #
✅ Background Location Tracking - Continues tracking even when app is killed
✅ Persistent Storage - Stores up to 1000 locations locally
✅ Real-time Updates - EventChannel streaming when app is active
✅ Server Sync - Optional HTTP POST to your backend
✅ Auto-restart - Service restarts after device reboot
✅ Battery Optimized - Configurable update intervals
✅ Android Foreground Service - Persistent notification keeps service alive
✅ iOS Significant Location Changes - Battery-efficient background tracking
Platform Support #
| Platform | Minimum Version |
|---|---|
| Android | API 21 (5.0) |
| iOS | 12.0 |
Installation #
Add this to your pubspec.yaml:
dependencies:
flutter_bg_location: ^0.0.1
permission_handler: ^11.0.0 # For permission management
Run:
flutter pub get
Platform Configuration #
Android Setup #
1. Update AndroidManifest.xml
Add to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<application>
<!-- Your existing application content -->
</application>
</manifest>
2. Gradle Configuration
Ensure minimum SDK in android/app/build.gradle:
android {
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
}
}
iOS Setup #
1. Update Info.plist
Add to ios/Runner/Info.plist:
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to your location in the background to track your movements even when the app is closed.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to your location in the background to track your movements even when the app is closed.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location to track your movements.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>fetch</string>
</array>
2. Enable Background Modes in Xcode
- Open
ios/Runner.xcworkspacein Xcode - Select Runner target
- Go to Signing & Capabilities
- Click + Capability → Background Modes
- Check: Location updates
Usage #
Basic Example #
import 'package:flutter/material.dart';
import 'package:flutter_bg_location/flutter_bg_location.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Location? _currentLocation;
bool _isTracking = false;
@override
void initState() {
super.initState();
_setupLocationTracking();
}
Future<void> _setupLocationTracking() async {
// Configure notification (Android only)
await FlutterBgLocation.setAndroidNotification(
title: 'Location Tracking',
message: 'Tracking your location in background',
icon: '@mipmap/ic_launcher',
);
// Set update interval (Android only, in milliseconds)
await FlutterBgLocation.setAndroidConfiguration(5000);
// Listen to real-time location updates
FlutterBgLocation.getLocationUpdates((location) {
setState(() {
_currentLocation = location;
});
print('Location: ${location.latitude}, ${location.longitude}');
});
}
Future<void> _requestPermissions() async {
// Request location permission
await Permission.location.request();
// Request background location (Always)
await Permission.locationAlways.request();
// Android: Request battery optimization exemption
await Permission.ignoreBatteryOptimizations.request();
}
Future<void> _startTracking() async {
await _requestPermissions();
bool started = await FlutterBgLocation.startLocationService(
distanceFilter: 10.0, // Update every 10 meters
);
setState(() {
_isTracking = started;
});
}
Future<void> _stopTracking() async {
await FlutterBgLocation.stopLocationService();
setState(() {
_isTracking = false;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Background Location')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_currentLocation?.toString() ?? 'No location'),
SizedBox(height: 20),
ElevatedButton(
onPressed: _isTracking ? _stopTracking : _startTracking,
child: Text(_isTracking ? 'Stop' : 'Start'),
),
],
),
),
),
);
}
}
API Reference #
FlutterBgLocation #
Main class for interacting with the plugin.
Methods
setAndroidNotification
Configure the foreground service notification (Android only).
static Future<void> setAndroidNotification({
required String title,
required String message,
String icon = '@mipmap/ic_launcher',
})
Parameters:
title(String, required): Notification titlemessage(String, required): Notification messageicon(String, optional): Icon resource name (default:@mipmap/ic_launcher)
Example:
await FlutterBgLocation.setAndroidNotification(
title: 'GPS Tracking',
message: 'Recording your route',
icon: '@mipmap/ic_notification',
);
setAndroidConfiguration
Set the location update interval (Android only).
static Future<void> setAndroidConfiguration(int interval)
Parameters:
interval(int): Update interval in milliseconds
Example:
await FlutterBgLocation.setAndroidConfiguration(10000); // 10 seconds
startLocationService
Start the background location tracking service.
static Future<bool> startLocationService({
double distanceFilter = 0,
bool forceAndroidLocationManager = false,
})
Parameters:
distanceFilter(double, optional): Minimum distance (in meters) before update (default: 0)forceAndroidLocationManager(bool, optional): Force use of LocationManager instead of FusedLocationProvider (default: false)
Returns:
Future<bool>: true if service started successfully
Example:
bool started = await FlutterBgLocation.startLocationService(
distanceFilter: 50.0, // Only update every 50 meters
);
stopLocationService
Stop the background location tracking service.
static Future<void> stopLocationService()
Example:
await FlutterBgLocation.stopLocationService();
isServiceRunning
Check if the location tracking service is currently running.
static Future<bool> isServiceRunning()
Returns:
Future<bool>: true if service is running
Example:
bool isRunning = await FlutterBgLocation.isServiceRunning();
print('Service status: $isRunning');
getLocationUpdates
Listen to real-time location updates (only works when app is active).
static void getLocationUpdates(Function(Location) callback)
Parameters:
callback(Function): Callback function receiving Location objects
Example:
FlutterBgLocation.getLocationUpdates((location) {
print('Lat: ${location.latitude}, Lng: ${location.longitude}');
print('Accuracy: ${location.accuracy}m');
print('Speed: ${location.speed}m/s');
});
getStoredLocations
Retrieve locations that were collected while the app was inactive.
static Future<List<Location>> getStoredLocations()
Returns:
Future<List<Location>>: List of stored Location objects (max 1000)
Example:
List<Location> locations = await FlutterBgLocation.getStoredLocations();
print('Found ${locations.length} stored locations');
for (var loc in locations) {
print('${loc.latitude}, ${loc.longitude} at ${DateTime.fromMillisecondsSinceEpoch(loc.time.toInt())}');
}
clearStoredLocations
Clear all stored locations from local storage.
static Future<void> clearStoredLocations()
Example:
await FlutterBgLocation.clearStoredLocations();
print('Storage cleared');
setCallbackUrl
Configure automatic HTTP POST of locations to your server.
static Future<void> setCallbackUrl(
String url, {
Map<String, String>? headers,
})
Parameters:
url(String): Your server endpoint URLheaders(Map<String, String>, optional): Custom HTTP headers
Example:
await FlutterBgLocation.setCallbackUrl(
'https://api.example.com/locations',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json',
},
);
Server Request Format:
{
"latitude": 37.7749,
"longitude": -122.4194,
"altitude": 10.5,
"accuracy": 5.0,
"bearing": 45.0,
"speed": 2.5,
"time": 1704326400000,
"is_mock": false
}
Location Class #
Represents a geographic location.
Properties
class Location {
final double latitude; // Latitude in degrees
final double longitude; // Longitude in degrees
final double altitude; // Altitude in meters above sea level
final double accuracy; // Accuracy in meters
final double bearing; // Direction of travel in degrees (0-360)
final double speed; // Speed in meters per second
final double time; // Timestamp in milliseconds since epoch
final bool isMock; // Whether this is a mock location (Android)
}
Methods
// Convert to Map
Map<String, dynamic> toMap()
// Create from Map
factory Location.fromMap(Map<dynamic, dynamic> map)
// String representation
String toString()
Example:
Location loc = location;
print('Coordinates: ${loc.latitude}, ${loc.longitude}');
print('Accuracy: ${loc.accuracy}m');
print('Speed: ${loc.speed * 3.6} km/h'); // Convert m/s to km/h
print('Time: ${DateTime.fromMillisecondsSinceEpoch(loc.time.toInt())}');
Permission Handling #
Android Permissions #
Future<void> requestAndroidPermissions() async {
// Step 1: Request fine location
var locationStatus = await Permission.location.request();
if (locationStatus.isGranted) {
// Step 2: Request background location (Android 10+)
await Permission.locationAlways.request();
// Step 3: Request battery optimization exemption
await Permission.ignoreBatteryOptimizations.request();
}
}
Android 10+ (API 29+): Background location must be requested separately after foreground location.
iOS Permissions #
Future<void> requestIOSPermissions() async {
// Step 1: Request "When In Use" permission
var status = await Permission.locationWhenInUse.request();
if (status.isGranted) {
// Step 2: Request "Always" permission
await Permission.locationAlways.request();
}
}
iOS: User must explicitly grant "Always" permission for background tracking.
Advanced Usage #
Complete Example with All Features #
import 'package:flutter/material.dart';
import 'package:flutter_bg_location/flutter_bg_location.dart';
import 'package:permission_handler/permission_handler.dart';
class LocationTracker extends StatefulWidget {
@override
_LocationTrackerState createState() => _LocationTrackerState();
}
class _LocationTrackerState extends State<LocationTracker> with WidgetsBindingObserver {
Location? _currentLocation;
bool _isTracking = false;
List<Location> _storedLocations = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initialize();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// App came back to foreground, reload stored locations
_loadStoredLocations();
}
}
Future<void> _initialize() async {
// Configure notification
await FlutterBgLocation.setAndroidNotification(
title: 'Location Tracking Active',
message: 'We are tracking your location',
icon: '@mipmap/ic_launcher',
);
// Configure update interval
await FlutterBgLocation.setAndroidConfiguration(5000);
// Setup server sync (optional)
await FlutterBgLocation.setCallbackUrl(
'https://your-api.com/locations',
headers: {'Authorization': 'Bearer YOUR_TOKEN'},
);
// Listen to updates
FlutterBgLocation.getLocationUpdates((location) {
setState(() {
_currentLocation = location;
});
});
// Check if already running
bool isRunning = await FlutterBgLocation.isServiceRunning();
setState(() {
_isTracking = isRunning;
});
// Load stored locations
await _loadStoredLocations();
}
Future<void> _loadStoredLocations() async {
List<Location> locations = await FlutterBgLocation.getStoredLocations();
setState(() {
_storedLocations = locations;
});
}
Future<void> _requestPermissions() async {
await Permission.location.request();
await Permission.locationAlways.request();
await Permission.ignoreBatteryOptimizations.request();
}
Future<void> _startTracking() async {
await _requestPermissions();
bool started = await FlutterBgLocation.startLocationService(
distanceFilter: 10.0,
);
if (started) {
setState(() {
_isTracking = true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tracking started')),
);
}
}
Future<void> _stopTracking() async {
await FlutterBgLocation.stopLocationService();
setState(() {
_isTracking = false;
});
}
Future<void> _clearLocations() async {
await FlutterBgLocation.clearStoredLocations();
await _loadStoredLocations();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Location Tracker'),
backgroundColor: _isTracking ? Colors.green : Colors.grey,
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status
Card(
child: ListTile(
leading: Icon(
_isTracking ? Icons.check_circle : Icons.cancel,
color: _isTracking ? Colors.green : Colors.grey,
),
title: Text(_isTracking ? 'Tracking Active' : 'Tracking Stopped'),
subtitle: Text('Works even when app is killed'),
),
),
SizedBox(height: 16),
// Current Location
Text('Current Location:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Card(
child: Padding(
padding: EdgeInsets.all(16),
child: _currentLocation != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Lat: ${_currentLocation!.latitude.toStringAsFixed(6)}'),
Text('Lng: ${_currentLocation!.longitude.toStringAsFixed(6)}'),
Text('Accuracy: ${_currentLocation!.accuracy.toStringAsFixed(2)}m'),
Text('Speed: ${(_currentLocation!.speed * 3.6).toStringAsFixed(2)} km/h'),
],
)
: Text('Waiting for location...'),
),
),
SizedBox(height: 16),
// Stored Locations
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Stored: ${_storedLocations.length}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
if (_storedLocations.isNotEmpty)
TextButton(
onPressed: _clearLocations,
child: Text('Clear'),
),
],
),
Expanded(
child: Card(
child: _storedLocations.isEmpty
? Center(child: Text('No stored locations'))
: ListView.builder(
itemCount: _storedLocations.length,
itemBuilder: (context, index) {
final loc = _storedLocations[index];
final time = DateTime.fromMillisecondsSinceEpoch(loc.time.toInt());
return ListTile(
leading: Icon(Icons.location_on),
title: Text('${loc.latitude.toStringAsFixed(4)}, ${loc.longitude.toStringAsFixed(4)}'),
subtitle: Text('${time.hour}:${time.minute}:${time.second}'),
);
},
),
),
),
SizedBox(height: 16),
// Controls
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isTracking ? null : _startTracking,
child: Text('Start'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _isTracking ? _stopTracking : null,
child: Text('Stop'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
),
],
),
SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _loadStoredLocations,
child: Text('Reload Stored Locations'),
),
),
],
),
),
);
}
}
Platform Behaviors #
Android #
| Feature | Behavior |
|---|---|
| Tracking Method | FusedLocationProvider (Google Play Services) |
| Update Frequency | Configurable (default: 5 seconds) |
| Foreground Service | Yes (persistent notification required) |
| Survives App Kill | Yes (if battery optimization disabled) |
| Auto-restart | Yes (on device reboot) |
| Battery Impact | Moderate (depends on update interval) |
iOS #
| Feature | Behavior |
|---|---|
| Tracking Method | CoreLocation (CLLocationManager) |
| Update Frequency | System-managed |
| Background Indicator | Blue status bar shown |
| Survives App Kill | Yes (significant location changes only) |
| Update Distance | ~500 meters (significant changes) |
| Battery Impact | Low (iOS optimized) |
Troubleshooting #
Android Issues #
Service stops when app is killed:
// Ensure battery optimization is disabled
await Permission.ignoreBatteryOptimizations.request();
No location updates:
- Check GPS is enabled on device
- Verify permissions granted (including background location)
- Test outdoors for better GPS signal
Notification doesn't appear:
- Verify
setAndroidNotification()is called beforestartLocationService() - Check notification permissions are granted
iOS Issues #
No background updates:
- Verify "Always" permission is granted
- Check Background Modes are enabled in Xcode
- Ensure Info.plist has all required keys
Blue bar disappears:
// Verify in FlutterBgLocationPlugin.swift:
locationManager?.allowsBackgroundLocationUpdates = true
locationManager?.pausesLocationUpdatesAutomatically = false
App not waking up:
- iOS uses significant location changes (500m threshold)
- Move more than 500 meters to trigger update
Best Practices #
Battery Optimization #
// Use larger distance filter
await FlutterBgLocation.startLocationService(
distanceFilter: 100.0, // Only update every 100 meters
);
// Increase update interval (Android)
await FlutterBgLocation.setAndroidConfiguration(30000); // 30 seconds
Data Management #
// Periodically sync to server and clear local storage
Future<void> syncLocations() async {
List<Location> locations = await FlutterBgLocation.getStoredLocations();
// Send to your server
await sendToServer(locations);
// Clear after successful sync
await FlutterBgLocation.clearStoredLocations();
}
User Experience #
// Always explain why you need location permission
Future<void> requestWithExplanation(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Location Permission'),
content: Text('We need your location to [explain your use case]'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Permission.location.request();
},
child: Text('OK'),
),
],
),
);
}
Testing #
Test on Physical Devices #
- ✅ Android: Use real device, not emulator
- ✅ iOS: Use real device (simulators have limited GPS)
Test Scenarios #
- Foreground tracking: App open, verify real-time updates
- Background tracking: App minimized, verify tracking continues
- Killed app tracking: Kill app completely, move around, reopen and check stored locations
- Reboot test: Restart device with tracking enabled, verify auto-restart
- Battery test: Monitor battery usage over extended period
Performance Metrics #
| Metric | Typical Value |
|---|---|
| Update latency | < 1 second |
| Memory usage | 10-30 MB |
| Battery drain (per hour) | 2-5% |
| Storage per location | ~200 bytes |
| Max stored locations | 1000 |
FAQ #
Q: Does this work when the app is completely killed?
A: Yes! Locations are stored locally and can be retrieved when the app reopens.
Q: What's the difference between Android and iOS tracking?
A: Android provides continuous updates; iOS uses significant location changes (every ~500m) when app is killed.
Q: How much battery does this use?
A: Varies by update frequency. Typical usage: 2-5% per hour. Use larger distance filters to reduce impact.
Q: Can I send locations to my server?
A: Yes! Use setCallbackUrl() to automatically POST locations to your backend.
Q: What happens if I reach 1000 stored locations?
A: Oldest locations are automatically removed to make room for new ones (FIFO).
Q: Do I need Google Play Services on Android?
A: By default yes, but you can use forceAndroidLocationManager: true to use the system LocationManager instead.
License #
MIT License - See LICENSE file for details
Contributing #
Contributions are welcome! Please submit pull requests to the GitHub repository.
Support #
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
Changelog #
0.0.1 #
- Initial release
- Android and iOS support
- Background location tracking
- Local storage up to 1000 locations
- Server sync capability
- Auto-restart on reboot
Made with ❤️ for the Flutter community
