Real-time Distance Tracking in Flutter with Geolocator

Real-time Distance Tracking in Flutter with Geolocator

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

Mobile applications that track movement, fitness activities, or navigation require reliable distance calculation capabilities. In this…

Mobile applications that track movement, fitness activities, or navigation require reliable distance calculation capabilities. In this post, we'll explore how to implement accurate distance tracking in Flutter using the Geolocator plugin. We'll build a robust distance calculation system that handles permissions, filters out GPS jitter, and provides reliable measurement for real-world applications.

The Geolocator package is a Flutter plugin that provides easy access to platform-specific location services. It handles the complexities of requesting permissions, accessing location data, and calculating distances between geographical coordinates.

First, add the dependency to your pubspec.yaml file:

geolocator: ^13.0.2

Building a Distance Calculation Service

Let's build a comprehensive service that handles:

  • Location permissions
  • Real-time distance tracking
  • GPS jitter filtering
  • Clean resource management

Here's our implementation:

import 'dart:async';
import 'package:geolocator/geolocator.dart';

class DistanceCalculation {
Position? _lastPosition;
double _totalDistance = 0.0; // Distance in KM
StreamSubscription<Position>? _positionStream;
bool _isTracking = false;

// Stream controller to emit permission status updates
final _permissionStatusController = StreamController<bool>.broadcast();
Stream<bool> get permissionStatusStream => _permissionStatusController.stream;

DistanceCalculation() {
// Check permission on initialization
checkPermission();
}

// Dispose method to clean up resources
void dispose() {
_positionStream?.cancel();
_permissionStatusController.close();
}

// Check and request location permission
Future<bool> checkPermission() async {
bool serviceEnabled;
LocationPermission permission;

// Test if location services are enabled
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
_permissionStatusController.add(false);
return false;
}

permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
_permissionStatusController.add(false);
return false;
}
}

if (permission == LocationPermission.deniedForever) {
_permissionStatusController.add(false);
return false;
}

_permissionStatusController.add(true);
return true;
}

// Check if tracking is currently active
bool get isTracking => _isTracking;

// Get current total distance in KM
double get currentDistance => _totalDistance;

// Start tracking distance
Future<bool> startTracking() async {
// First check for permission
bool hasPermission = await checkPermission();
if (!hasPermission) {
return false;
}

// Reset tracking values
_totalDistance = 0.0;
_lastPosition = null;
_isTracking = true;

// Configure location settings for better accuracy
LocationSettings locationSettings = const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 5, // Updates every 5 meters for better accuracy
);

// Start position stream
_positionStream = Geolocator.getPositionStream(locationSettings: locationSettings).listen((Position position) {
if (_lastPosition != null) {
// Calculate distance between points
double distanceInMeters = Geolocator.distanceBetween(
_lastPosition!.latitude, _lastPosition!.longitude,
position.latitude, position.longitude,
);

// Filter out small movements (likely GPS jitter)
if (distanceInMeters > 2) {
_totalDistance += distanceInMeters / 1000; // Convert meters to KM
}
}

_lastPosition = position;
});

return true;
}

// Stop tracking and return total distance in KM
double stopTracking() {
_positionStream?.cancel();
_positionStream = null;
_isTracking = false;

return _totalDistance;
}

// Reset tracking without stopping
void resetDistance() {
_totalDistance = 0.0;
_lastPosition = null;
}

// Get last known position
Position? get lastPosition => _lastPosition;
}

Key Features Explained

1. Permission Handling

The checkPermission() method manages all aspects of location permissions:

  • Checks if location services are enabled
  • Verifies current permission status
  • Requests permissions if needed
  • Returns and broadcasts permission status

This comprehensive approach ensures your app handles location access properly across different platforms.

2. Distance Calculation Algorithm

Our distance calculation happens in the position stream listener:

double distanceInMeters = Geolocator.distanceBetween(
_lastPosition!.latitude, _lastPosition!.longitude,
position.latitude, position.longitude,
);

The distanceBetween method uses the Haversine formula, which calculates the shortest distance between two points on a sphere (Earth). This is more accurate than simpler calculation methods, especially for longer distances.

3. GPS Jitter Filtering

GPS readings can be noisy, especially when stationary. Our implementation includes a jitter filter:

// Filter out small movements (likely GPS jitter)
if (distanceInMeters > 2) {
_totalDistance += distanceInMeters / 1000;
}

By ignoring movements under 2 meters, we prevent adding up tiny fluctuations that would artificially inflate the total distance.

4. Optimized Location Settings

We configure the location service for our needs:

LocationSettings locationSettings = const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 5, // Updates every 5 meters
);
  • LocationAccuracy.high: Provides more accurate position data
  • distanceFilter: 5: Only receives updates when the user moves 5 meters or more, reducing battery usage while maintaining reasonable accuracy

Using the Distance Calculation Service

Singleton Implementation

For apps that need to access distance calculation from multiple places, implement a singleton pattern:

class DistanceCalculation {
static final DistanceCalculation _instance = DistanceCalculation._internal();

factory DistanceCalculation() {
return _instance;
}

DistanceCalculation._internal() {
checkPermission();
}

// Rest of the implementation remains the same
}

This ensures that anywhere you call DistanceCalculation(), you get the same instance, maintaining state across your app.

Basic Usage

// Initialize and start tracking
final distanceTracker = DistanceCalculation();
await distanceTracker.startTracking();

// Later, get the current distance
double currentKm = distanceTracker.currentDistance;

// When finished, stop tracking and get the final distance
double totalKm = distanceTracker.stopTracking();

// Clean up resources when done
distanceTracker.dispose();

Displaying Real-time Updates

class _MyAppState extends State<MyApp> {
final DistanceCalculation _distanceTracker = DistanceCalculation();
Timer? _updateTimer;
double _currentDistance = 0.0;

@override
void initState() {
super.initState();
_startTracking();

// Update UI every second
_updateTimer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
_currentDistance = _distanceTracker.currentDistance;
});
});
}

Future<void> _startTracking() async {
await _distanceTracker.startTracking();
}

@override
void dispose() {
_updateTimer?.cancel();
_distanceTracker.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'Distance traveled: ${_currentDistance.toStringAsFixed(2)} km',
style: TextStyle(fontSize: 24),
),
),
);
}
}

Common Challenges and Solutions

1. Background Execution

To continue tracking when the app is in the background, you'll need to:

  • Configure background location permissions in your app's manifest files
  • Use a background service or isolate on Android
  • Implement background modes on iOS

2. Battery Optimization

Location tracking can be battery-intensive. Consider:

  • Adjusting the distanceFilter based on the activity type
  • Lowering accuracy for less critical applications
  • Implementing adaptive sampling rates

3. Cross-platform Differences

Be aware that iOS and Android have different permission models and behaviors:

  • iOS requires explicit permission strings in Info.plist
  • Android needs manifest permissions and potentially runtime permission handling

The Geolocator package provides a powerful foundation for distance tracking in Flutter applications. Our implementation enhances it with permission management, jitter filtering, and clean resource handling.

Remember that accurate distance calculation is more than just adding up GPS points — it requires thoughtful handling of permissions, careful filtering of noise, and proper position stream management. With these considerations in mind, you can build location-aware apps that provide reliable distance measurements for navigation, fitness tracking, or any movement-based functionality.

What location-based features are you implementing in your Flutter apps? Share your experiences in the comments below!

Report Page