Perché geolocator
Dalle app di delivery ai fitness tracker, dalla ricerca di punti di interesse alla geofencing, la posizione dell'utente è uno dei dati più richiesti in mobile. In Flutter il pacchetto di riferimento è geolocator, mantenuto dal team Baseflow: offre un'API unificata su Android, iOS, web, macOS e Windows, gestisce i permessi runtime, espone uno stream di posizioni e permette di configurare accuratezza, filtri di distanza e comportamento in background.
In questa guida vediamo come integrarlo correttamente, evitando gli errori più comuni (permessi mancanti, stream che si spengono, batteria prosciugata).
Installazione e configurazione
Aggiungiamo le dipendenze:
dependencies:
geolocator: ^13.0.2
geocoding: ^3.0.0 # opzionale, per reverse geocoding
Android
Nel file android/app/src/main/AndroidManifest.xml, dentro il tag <manifest>:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Solo se serve il tracking a schermo spento / app in background -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
Ricorda che compileSdk deve essere almeno 34 e che su Android 12+ l'utente può concedere solo la posizione approssimata: in quel caso ACCESS_FINE_LOCATION risulta negato anche se l'app riceve comunque coordinate a bassa precisione.
iOS
In ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Usiamo la tua posizione per mostrarti i locali più vicini.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Usiamo la posizione anche in background per tracciare i tuoi percorsi.</string>
Se serve il background, abilita in Xcode Signing & Capabilities → Background Modes → Location updates. Le descrizioni devono essere specifiche: Apple rifiuta le app con testi generici tipo "serve la posizione".
Il flusso corretto dei permessi
Prima di chiedere una posizione bisogna sempre verificare due cose distinte: che il servizio di localizzazione del dispositivo sia acceso e che l'app abbia il permesso.
import 'package:geolocator/geolocator.dart';
Future<void> ensureLocationReady() async {
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
// Opzionale: Geolocator.openLocationSettings();
throw const LocationException('Servizi di localizzazione disattivati');
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever) {
// L'utente ha bloccato definitivamente: si può solo aprire le impostazioni
await Geolocator.openAppSettings();
throw const LocationException('Permesso negato permanentemente');
}
if (permission == LocationPermission.denied) {
throw const LocationException('Permesso negato');
}
}
class LocationException implements Exception {
const LocationException(this.message);
final String message;
@override
String toString() => message;
}
Chiedi il permesso nel contesto giusto: non all'avvio dell'app, ma quando l'utente tocca "Trova vicino a me". Il tasso di accettazione cambia drasticamente.
Ottenere la posizione corrente
Future<Position> getPosition() async {
await ensureLocationReady();
return Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
timeLimit: Duration(seconds: 10),
),
);
}
Il timeLimit è fondamentale: senza di esso, in una zona con segnale GPS scarso la Future può restare pendente a lungo. In caso di scadenza viene lanciata una TimeoutException, quindi conviene avere un piano B:
Future<Position?> getPositionWithFallback() async {
try {
return await getPosition();
} on TimeoutException {
// Ultima posizione nota: istantanea, ma potenzialmente vecchia
return Geolocator.getLastKnownPosition();
}
}
L'oggetto Position contiene latitude, longitude, accuracy (in metri), altitude, speed, heading, timestamp e i flag isMocked (utile per bloccare i fake GPS in app sensibili come i time tracker).
I livelli di accuratezza
| Valore | Precisione tipica | Uso consigliato |
|---|---|---|
lowest / low |
500 m – 3 km | Meteo, contenuti regionali |
medium |
~100 m | Suggerimenti di città/quartiere |
high |
~10 m | Ricerca POI, mappe |
best / bestForNavigation |
< 5 m | Navigazione turn-by-turn, sport |
Ogni scalino verso l'alto costa batteria: usa high come default e sali solo quando serve davvero.
Stream continuo di posizioni
Per il tracking usa getPositionStream, configurando impostazioni specifiche per piattaforma:
Stream<Position> trackPosition() {
late final LocationSettings settings;
if (defaultTargetPlatform == TargetPlatform.android) {
settings = AndroidSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 25, // metri minimi tra due aggiornamenti
intervalDuration: const Duration(seconds: 5),
foregroundNotificationConfig: const ForegroundNotificationConfig(
notificationTitle: 'Tracciamento attivo',
notificationText: 'Stiamo registrando il tuo percorso',
enableWakeLock: true,
),
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
settings = AppleSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 25,
pauseLocationUpdatesAutomatically: true,
showBackgroundLocationIndicator: true,
allowBackgroundLocationUpdates: true,
activityType: ActivityType.fitness,
);
} else {
settings = const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 25,
);
}
return Geolocator.getPositionStream(locationSettings: settings);
}
Il distanceFilter è la leva più efficace sul consumo: se l'utente è fermo, non arrivano eventi e la radio GPS lavora meno.
Su Android il foregroundNotificationConfig crea un foreground service e mostra una notifica persistente: senza di esso il sistema uccide gli aggiornamenti pochi minuti dopo che l'app va in background.
Consumare lo stream nella UI
class TrackingPage extends StatefulWidget {
const TrackingPage({super.key});
@override
State<TrackingPage> createState() => _TrackingPageState();
}
class _TrackingPageState extends State<TrackingPage> {
StreamSubscription<Position>? _sub;
final _points = <Position>[];
Future<void> _start() async {
await ensureLocationReady();
_sub?.cancel();
_sub = trackPosition().listen(
(p) => setState(() => _points.add(p)),
onError: (Object e) => debugPrint('Errore GPS: $e'),
);
}
@override
void dispose() {
_sub?.cancel(); // fondamentale: evita leak e consumo inutile
super.dispose();
}
@override
Widget build(BuildContext context) {
final distance = _totalDistance();
return Scaffold(
appBar: AppBar(title: const Text('Tracking')),
body: Center(
child: Text('${(distance / 1000).toStringAsFixed(2)} km'),
),
floatingActionButton: FloatingActionButton(
onPressed: _start,
child: const Icon(Icons.play_arrow),
),
);
}
double _totalDistance() {
var total = 0.0;
for (var i = 1; i < _points.length; i++) {
total += Geolocator.distanceBetween(
_points[i - 1].latitude, _points[i - 1].longitude,
_points[i].latitude, _points[i].longitude,
);
}
return total;
}
}
Se usi Riverpod, lo stream si incapsula bene in uno StreamProvider: la cancellazione della subscription avviene automaticamente quando il provider viene dismesso.
Distanze, direzione e filtri di qualità
Geolocator espone due utility statiche molto usate:
final metri = Geolocator.distanceBetween(45.4642, 9.1900, 41.9028, 12.4964);
final gradi = Geolocator.bearingBetween(45.4642, 9.1900, 41.9028, 12.4964);
Nel tracking reale conviene scartare i campioni rumorosi, altrimenti il percorso registrato diventa una scia di zig-zag:
bool isReliable(Position p, Position? previous) {
if (p.accuracy > 50) return false; // troppo impreciso
if (p.isMocked) return false;
if (previous == null) return true;
final seconds = p.timestamp.difference(previous.timestamp).inSeconds;
if (seconds <= 0) return false;
final d = Geolocator.distanceBetween(
previous.latitude, previous.longitude, p.latitude, p.longitude,
);
return d / seconds < 55; // ~200 km/h: oltre è un salto irreale
}
Da coordinate a indirizzo: il geocoding
geolocator non fa geocoding; per questo esiste il pacchetto gemello geocoding:
import 'package:geocoding/geocoding.dart';
Future<String> addressOf(Position p) async {
final places = await placemarkFromCoordinates(p.latitude, p.longitude);
final place = places.first;
return '${place.street}, ${place.locality} (${place.administrativeArea})';
}
Usa il geocoding con parsimonia: è una chiamata di sistema che può richiedere rete e ha limiti di rate. Mettila in cache per coordinate arrotondate a 4 decimali (~11 metri).
Strategie per la batteria
- Chiudi sempre lo stream quando l'utente lascia la schermata o mette in pausa il tracking.
- Alza il
distanceFiltere abbassa l'accuratezza quando l'app va in background, se il caso d'uso lo consente. - Preferisci
getLastKnownPosition()per il primo render della UI e poi aggiorna con la posizione fresca. - Su iOS attiva
pauseLocationUpdatesAutomaticallye imposta l'activityTypecorretto: il sistema sospende gli aggiornamenti quando rileva che l'utente è fermo. - Evita di richiedere
ACCESS_BACKGROUND_LOCATIONse non è indispensabile: sia Google Play sia App Store richiedono una giustificazione dettagliata in fase di review.
Testare il codice che usa la posizione
Non dipendere direttamente dalle API statiche di Geolocator nei widget: incapsulale in un servizio con interfaccia, così i test diventano banali.
abstract interface class LocationService {
Future<Position> current();
Stream<Position> watch();
}
class GeolocatorLocationService implements LocationService {
@override
Future<Position> current() => getPosition();
@override
Stream<Position> watch() => trackPosition();
}
// Nei test
class FakeLocationService implements LocationService {
@override
Future<Position> current() async => Position(
latitude: 45.4642, longitude: 9.19, timestamp: DateTime.now(),
accuracy: 5, altitude: 120, altitudeAccuracy: 3, heading: 0,
headingAccuracy: 1, speed: 0, speedAccuracy: 0,
);
@override
Stream<Position> watch() => Stream.fromIterable([]);
}
Per i test manuali, su iOS puoi simulare percorsi da Xcode (Debug → Simulate Location), su Android dall'emulatore (Extended controls → Location, con import di file GPX/KML).
Errori comuni da evitare
- Chiamare
getCurrentPositionsenza controllare i permessi: su iOS la Future resta pendente finché l'utente non risponde, su Android lancia un'eccezione. - Dimenticare
deniedForever: continuare a chiamarerequestPermission()non mostra più alcun dialog, l'unica via èopenAppSettings(). - Non cancellare la subscription: la radio GPS resta attiva e la batteria crolla.
- Assumere che l'accuratezza sia costante: sempre validare
position.accuracyprima di prendere decisioni di business. - Nessun fallback offline: se il GPS non aggancia, mostra all'utente un messaggio chiaro e un'alternativa (inserimento manuale dell'indirizzo).
Conclusioni
geolocator copre il 90% dei casi d'uso legati alla posizione con un'API pulita e multipiattaforma. Le chiavi per un'integrazione solida sono tre: un flusso di permessi esplicito e contestuale, impostazioni di accuratezza calibrate sul caso d'uso reale e una gestione rigorosa del ciclo di vita degli stream. Se il tuo scenario richiede geofencing o tracking persistente anche a app terminata, valuta soluzioni complementari come flutter_background_geolocation, ma per tutto il resto geolocator è la scelta più semplice e mantenibile.