Live Location Tracking in Flutter with Google Maps
Building real-time location features in Flutter — Google Maps setup, streaming GPS updates efficiently, smooth marker movement, geocoding and the battery and privacy details that matter.
As lead Flutter developer at Mascopia, a big part of my work was location: showing people and places on a map, tracking movement live, and turning coordinates into addresses people understand. Location features look simple in a demo and get complicated fast in production — battery drain, permissions, jittery markers, and API bills.
This post covers how I build live tracking in Flutter with google_maps_flutter, geolocator and geocoding, and the details that separate a smooth experience from a janky one.
Setup: API keys, restricted properly
You need a Google Maps API key with the Maps SDK for Android and Maps SDK for iOS enabled. Add it to AndroidManifest.xml and AppDelegate.swift as described in the package README.
Then, before anything else, restrict the key in Google Cloud Console:
- Android key: restricted to your package name and SHA-1 signing fingerprints (debug and release — and the Play App Signing key if you use it).
- iOS key: restricted to your bundle identifier.
- API restrictions: only the APIs that key actually uses.
Map keys ship inside your app, so anyone can extract them. Restrictions are what stop someone else from running up your bill. Set a budget alert too.
Asking for location the right way
Location is the permission users are most wary of. I follow three rules:
- Ask when it’s needed, on the screen that uses it, with a short explanation first.
- Ask for “while in use” first. Only request background location if the feature genuinely needs it — and be prepared to justify it in store review.
- Handle every outcome, including “denied forever” and “location services turned off”.
Future<bool> ensureLocationPermission() async {
if (!await Geolocator.isLocationServiceEnabled()) {
// Offer a button that calls Geolocator.openLocationSettings()
return false;
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever) {
// The system dialog won't show again — offer Geolocator.openAppSettings()
return false;
}
return permission == LocationPermission.whileInUse ||
permission == LocationPermission.always;
}
Streaming position updates efficiently
The naive approach — polling getCurrentPosition() on a timer — is bad for battery and gives uneven results. Use a position stream with a distance filter instead:
final positionStream = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10, // meters moved before a new update is emitted
),
);
The distanceFilter is the most important battery setting you have. If the user is standing still, you get no updates at all. Tune it to the feature: a delivery tracker might use 10–20 meters; a “nearby places” screen might use 100 or more.
Accuracy matters too. LocationAccuracy.high uses GPS and costs battery; medium or low rely more on Wi-Fi and cell towers. Use the lowest accuracy that makes the feature work.
Sending your location to others
For tracking features where other users see your position, the device streams updates to a backend (Firebase, a WebSocket server, SignalR — whatever the project uses), and other devices subscribe.
Don’t send every update. Throttle outbound updates to a sensible rate, for example no more than once every few seconds, and skip updates where the position has barely changed. Your backend, your users’ data plans and your cloud bill will all thank you.
Smooth marker movement
When a new position arrives for a tracked person, the simple thing is to replace the marker at the new coordinates. The result is a marker that teleports every few seconds, which looks broken.
Instead, animate between the old and new positions:
class _TrackerState extends State<Tracker> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
late LatLng _from = widget.initial;
late LatLng _to = widget.initial;
void moveTo(LatLng next) {
_from = _current;
_to = next;
_controller.forward(from: 0);
}
LatLng get _current {
final t = Curves.easeInOut.transform(_controller.value);
return LatLng(
_from.latitude + (_to.latitude - _from.latitude) * t,
_from.longitude + (_to.longitude - _from.longitude) * t,
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) => GoogleMap(
initialCameraPosition: CameraPosition(target: widget.initial, zoom: 16),
markers: {Marker(markerId: const MarkerId('driver'), position: _current)},
),
);
}
}
Linear interpolation between two points is perfectly fine for the short distances between updates. For a nicer touch, rotate the marker to face the direction of travel using Geolocator.bearingBetween(...) and the marker’s rotation property.
For performance, keep the GoogleMap widget itself stable and only update the markers set. Rebuilding the entire map widget unnecessarily can cause flicker on some devices.
Following the user with the camera
For “follow me” behavior, move the camera with the GoogleMapController:
await mapController.animateCamera(CameraUpdate.newLatLng(position));
Stop following as soon as the user drags the map (listen to onCameraMoveStarted) and show a “re-center” button. Nothing is more frustrating than a map that keeps snapping back while you’re trying to look around.
Turning coordinates into addresses
Users don’t think in latitude and longitude. The geocoding package converts between coordinates and addresses using the platform’s built-in geocoder:
final placemarks = await placemarkFromCoordinates(lat, lng);
final p = placemarks.first;
final label = [p.street, p.locality].where((s) => s != null && s.isNotEmpty).join(', ');
Geocoding is relatively slow and can be rate-limited, so:
- Don’t reverse-geocode every GPS update. Do it when the user stops moving or when they tap a point.
- Cache results keyed by rounded coordinates.
- Handle failures — show coordinates or “Unknown location” rather than an error.
Background tracking
If your feature must track location while the app is in the background, it becomes a different project:
- Android requires a foreground service with a persistent notification.
geolocatorsupports this viaAndroidSettingsandforegroundNotificationConfig. - iOS requires the background location mode and an “Always” permission, and Apple reviews this closely.
- Both stores ask you to justify background location in review, and Google Play requires a declaration form.
My advice: only go there if the product truly needs it. “While in use” covers far more features than people expect.
Takeaways
- Restrict your map API keys and set budget alerts on day one.
- Ask for location in context, start with “while in use”, and handle every denial.
- Use a position stream with a distance filter, not a polling timer.
- Throttle updates you send to the server.
- Animate markers between positions; don’t let them teleport.
- Reverse-geocode sparingly and cache the results.
Location features are one of the clearest examples of the gap between “works in the simulator” and “works in someone’s pocket all day”. If you’re building something location-based, let’s talk.
Comments
Questions, corrections or your own experience — leave a comment below (GitHub sign-in).