Showing the User's Location on the Map
Showing the User's Location on the Map
One of the most compelling features in any location-aware app is displaying where the user is right now — and keeping that indicator moving in real time as they travel. In this lesson you will wire together two packages you already know — geolocator and google_maps_flutter — to centre the camera on the user's live position, enable the built-in My Location layer, and drive a custom marker that updates on every new GPS fix.
Why Not Just Call getCurrentPosition Once?
Fetching a single position on startup is fine for one-shot use cases (e.g. "find restaurants near me"), but for a live tracking experience you need a continuous stream. Geolocator.getPositionStream() returns a Stream<Position> that emits a new event whenever the device moves beyond your configured distance filter. Each event carries fresh latitude, longitude, accuracy, speed, and heading fields.
myLocationEnabled: true on the GoogleMap widget renders Google's built-in blue-dot layer and the "centre on me" button. You still need to manage your own StreamSubscription if you want to react to position changes in Dart (e.g. animate the camera, update a database, or display an accuracy circle).Setting Up the GoogleMapController
To programmatically move the camera you need a reference to the GoogleMapController that is handed to you in the onMapCreated callback. Store it in a Completer so any subsequent code can await it safely:
Storing the Map Controller
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
class LiveLocationMap extends StatefulWidget {
const LiveLocationMap({super.key});
@override
State<LiveLocationMap> createState() => _LiveLocationMapState();
}
class _LiveLocationMapState extends State<LiveLocationMap> {
final Completer<GoogleMapController> _mapController = Completer();
StreamSubscription<Position>? _positionSub;
LatLng _currentLatLng = const LatLng(0, 0);
Set<Marker> _markers = {};
@override
void initState() {
super.initState();
_startTracking();
}
@override
void dispose() {
_positionSub?.cancel();
super.dispose();
}
// ...
}
Using a Completer is important because onMapCreated fires asynchronously after the widget is first rendered. Any code that calls _mapController.future will simply wait until the controller is ready, avoiding null-reference crashes.
Subscribing to the Position Stream
Call Geolocator.getPositionStream() inside initState (after confirming permissions, which you handled in Lesson 5). Supply a LocationSettings object to tune the accuracy and distance filter. On each emission, animate the map camera and refresh the marker inside setState:
Stream-Based Live Tracking
Future<void> _startTracking() async {
// Assumes permission is already granted (see Lesson 5)
const locationSettings = LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10, // metres between updates
);
_positionSub = Geolocator.getPositionStream(
locationSettings: locationSettings,
).listen((Position position) async {
final latLng = LatLng(position.latitude, position.longitude);
// Animate the camera to follow the user
final controller = await _mapController.future;
await controller.animateCamera(
CameraUpdate.newLatLng(latLng),
);
// Update the marker and rebuild the widget
setState(() {
_currentLatLng = latLng;
_markers = {
Marker(
markerId: const MarkerId('user_location'),
position: latLng,
infoWindow: InfoWindow(
title: 'You are here',
snippet:
'${position.latitude.toStringAsFixed(5)}, '
'${position.longitude.toStringAsFixed(5)}',
),
),
};
});
});
}
distanceFilter above 0 (e.g. 10 m) to avoid a flood of near-identical events that waste battery and trigger unnecessary rebuilds. For pedestrian navigation 10–15 m is a good default; for vehicle tracking 30–50 m is typical.Building the GoogleMap Widget
Pass the controller completer, enable the My Location layer, and supply the markers set. Setting myLocationButtonEnabled: false hides Google's default re-centre button so you can provide your own UI if desired:
GoogleMap Widget with myLocationEnabled
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Live Location')),
body: GoogleMap(
onMapCreated: (GoogleMapController controller) {
if (!_mapController.isCompleted) {
_mapController.complete(controller);
}
},
initialCameraPosition: CameraPosition(
target: _currentLatLng,
zoom: 16,
),
myLocationEnabled: true, // blue dot + accuracy ring
myLocationButtonEnabled: false, // hide default button
markers: _markers,
zoomControlsEnabled: true,
),
floatingActionButton: FloatingActionButton(
onPressed: _centreOnUser,
child: const Icon(Icons.my_location),
),
);
}
Future<void> _centreOnUser() async {
final controller = await _mapController.future;
await controller.animateCamera(
CameraUpdate.newLatLngZoom(_currentLatLng, 16),
);
}
Handling Errors and Edge Cases
- Permission denied at runtime: wrap
_startTracking()with a try/catch forPermissionDeniedExceptionand show a snack bar. - Location services disabled: catch
LocationServiceDisabledExceptionand prompt the user to open Settings. - Stream error events: supply the optional
onErrorparameter to thelistencall to avoid unhandled exceptions silently killing the stream. - Widget disposed before controller ready: check
mountedbefore callingsetStateafter anyawait.
_positionSub?.cancel() inside dispose(). Forgetting to cancel the subscription keeps the GPS sensor active after the widget is removed from the tree, draining the user's battery and potentially causing setState-on-disposed-widget errors.Summary
In this lesson you learned how to combine geolocator's position stream with google_maps_flutter's camera API to build a live user-location tracker. The key steps are: obtain a GoogleMapController via a Completer, subscribe to Geolocator.getPositionStream(), animate the camera on each emission, update the marker inside setState, enable myLocationEnabled: true for the built-in blue dot, and always cancel the subscription in dispose(). With these building blocks you can extend the map to show accuracy circles, breadcrumb trails, or turn-by-turn overlays.