Maps, Location & Device Features

Getting Device Location with geolocator

16 min Lesson 6 of 12

Getting Device Location with geolocator

The geolocator package is the most widely used Flutter plugin for accessing the device's GPS and network-based positioning system. It provides a clean, cross-platform Dart API for both one-shot position requests and continuous location streams, returning a rich Position object that includes latitude, longitude, altitude, accuracy, speed, and heading.

Note: Location is a sensitive permission. On Android and iOS the user must explicitly grant location access at runtime. The geolocator package handles the permission prompts, but you are still responsible for declaring the correct entries in AndroidManifest.xml and Info.plist.

Package Setup

Add the dependency to pubspec.yaml and run flutter pub get:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  geolocator: ^13.0.0

For Android, add the following permissions inside the <manifest> tag of android/app/src/main/AndroidManifest.xml:

AndroidManifest.xml permissions

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Only if you need background location -->
<!-- <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> -->

For iOS, add the following keys to ios/Runner/Info.plist:

Info.plist keys

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location to show nearby places.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs background location for tracking.</string>

Checking and Requesting Permissions

Before requesting position data you must verify that location services are enabled on the device and that your app has the necessary permission. The geolocator API provides dedicated helpers for both checks:

Permission check and request

import 'package:geolocator/geolocator.dart';

Future<void> _checkPermissions() async {
  // 1. Is the location hardware/service enabled?
  bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    // Location services are turned off in device settings
    throw Exception('Location services are disabled.');
  }

  // 2. Has the user granted permission to this app?
  LocationPermission permission = await Geolocator.checkPermission();

  if (permission == LocationPermission.denied) {
    // First-time or after the user previously denied
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      throw Exception('Location permission denied.');
    }
  }

  if (permission == LocationPermission.deniedForever) {
    // User selected "Never" – must open app settings manually
    throw Exception('Location permission permanently denied.');
  }
  // Permission is whileInUse or always – safe to proceed
}

One-Shot Position Request

Use Geolocator.getCurrentPosition() to fetch a single Position snapshot. This is ideal for features like "find nearby restaurants" where you only need the location once. You can tune accuracy vs battery cost with the LocationAccuracy enum:

  • LocationAccuracy.lowest — ~3 km accuracy, minimal battery
  • LocationAccuracy.low — ~1 km accuracy
  • LocationAccuracy.medium — ~100 m accuracy
  • LocationAccuracy.high — ~10 m accuracy (GPS on)
  • LocationAccuracy.best — maximum accuracy, highest battery cost
  • LocationAccuracy.bestForNavigation — like best but also uses motion sensors

getCurrentPosition example

import 'package:geolocator/geolocator.dart';

class LocationService {
  /// Returns the device's current position after checking permissions.
  static Future<Position> determinePosition() async {
    bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      return Future.error('Location services are disabled.');
    }

    LocationPermission permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        return Future.error('Location permissions are denied');
      }
    }

    if (permission == LocationPermission.deniedForever) {
      return Future.error(
        'Location permissions are permanently denied, '
        'we cannot request permissions.',
      );
    }

    // At this point permissions are granted and we can
    // continue accessing the position of the device.
    return await Geolocator.getCurrentPosition(
      locationSettings: const LocationSettings(
        accuracy: LocationAccuracy.high,
        timeLimit: Duration(seconds: 15),
      ),
    );
  }
}

// Usage in a StatefulWidget
Future<void> _fetchLocation() async {
  try {
    final Position position = await LocationService.determinePosition();
    print('Latitude:  ${position.latitude}');
    print('Longitude: ${position.longitude}');
    print('Accuracy:  ${position.accuracy} m');
    print('Altitude:  ${position.altitude} m');
    print('Speed:     ${position.speed} m/s');
  } catch (e) {
    print('Error: $e');
  }
}
Tip: Always set a timeLimit on getCurrentPosition(). Without it the Future can hang indefinitely if the GPS cannot get a fix (e.g., indoors or in a tunnel). A 10–15 second timeout with a graceful fallback provides a much better user experience.

Continuous Location Stream

Use Geolocator.getPositionStream() for real-time tracking use-cases such as navigation, fitness apps, or live delivery tracking. It returns a Stream<Position> that emits new values whenever the device moves beyond the configured distance filter or interval. Always cancel the stream subscription when the widget is disposed to avoid memory leaks:

getPositionStream with StreamSubscription

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

class LiveLocationWidget extends StatefulWidget {
  const LiveLocationWidget({super.key});

  @override
  State<LiveLocationWidget> createState() => _LiveLocationWidgetState();
}

class _LiveLocationWidgetState extends State<LiveLocationWidget> {
  StreamSubscription<Position>? _positionSubscription;
  Position? _currentPosition;
  String _statusMessage = 'Waiting for location...';

  @override
  void initState() {
    super.initState();
    _startLocationStream();
  }

  void _startLocationStream() {
    const locationSettings = LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 10, // emit only when moved >= 10 metres
    );

    _positionSubscription =
        Geolocator.getPositionStream(locationSettings: locationSettings)
            .listen(
      (Position position) {
        setState(() {
          _currentPosition = position;
          _statusMessage =
              'Lat: ${position.latitude.toStringAsFixed(6)}, '
              'Lng: ${position.longitude.toStringAsFixed(6)}';
        });
      },
      onError: (Object e) {
        setState(() {
          _statusMessage = 'Stream error: $e';
        });
      },
    );
  }

  @override
  void dispose() {
    // Critical: cancel the subscription to stop GPS hardware
    _positionSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(_statusMessage, textAlign: TextAlign.center),
        if (_currentPosition != null) ...[
          Text('Accuracy: ${_currentPosition!.accuracy.toStringAsFixed(1)} m'),
          Text('Altitude: ${_currentPosition!.altitude.toStringAsFixed(1)} m'),
          Text('Speed:    ${_currentPosition!.speed.toStringAsFixed(2)} m/s'),
        ],
      ],
    );
  }
}
Warning: Forgetting to call _positionSubscription?.cancel() in dispose() keeps the GPS active after the widget is removed from the tree. This drains the battery and can cause setState calls on a disposed widget, leading to errors at runtime.

The Position Object

Both getCurrentPosition() and getPositionStream() yield a Position object with the following key properties:

  • latitude / longitude — coordinates in decimal degrees (WGS-84)
  • accuracy — estimated horizontal accuracy in metres
  • altitude — height above sea level in metres
  • altitudeAccuracy — estimated vertical accuracy in metres (iOS / Android 13+)
  • speed — ground speed in metres per second
  • speedAccuracy — estimated speed accuracy in metres per second
  • heading — direction of travel in degrees (0 = north, clockwise)
  • timestampDateTime when the fix was obtained

Summary

The geolocator package gives you everything needed to work with device location in Flutter. Use isLocationServiceEnabled() and checkPermission() / requestPermission() before accessing any position data. Call getCurrentPosition() for one-shot fixes with a sensible timeLimit, and getPositionStream() with an appropriate distanceFilter for live tracking. Always cancel your StreamSubscription in dispose() to protect battery life and prevent widget lifecycle errors.