Flutter Geolocation With geolocator: Get User Location

Coding Liquids tutorial cover featuring Sagnik Bhattacharya for Flutter geolocation with geolocator.
Coding Liquids tutorial cover featuring Sagnik Bhattacharya for Flutter geolocation with geolocator.

This tutorial builds a location screen that can obtain one reliable position and then follow the device as it moves. It accounts for the two separate gates that frequently confuse location implementations: the device location service and the permission granted to your app.

Follow me on Instagram@sagnikteaches

You will configure Android and iOS, inspect LocationPermission, request access only when appropriate, call getCurrentPosition with explicit accuracy settings, and consume getPositionStream without leaking a subscription. The failure paths include disabled services, ordinary denial, permanent denial, timeouts, and unavailable fixes.

Connect on LinkedInSagnik Bhattacharya

Add the example to a real Flutter application and test it on a physical device where possible. Simulators can supply test coordinates, but they do not reproduce every permission screen, GPS delay, or reduced-accuracy behaviour seen on users' phones.

Subscribe on YouTube@codingliquids
The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Install geolocator and declare why the app needs location

Add the package through Flutter so that your project receives a compatible current version:

flutter pub add geolocator

Native permission declarations are still required. For Android, place coarse and fine location permissions directly inside the root <manifest> element in android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <application
        android:label="location_example"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <!-- Existing activities and metadata remain here. -->
    </application>
</manifest>

Coarse permission permits approximate results, while fine permission allows the user to grant more precise access. Declaring either permission does not grant it; Android still controls the runtime prompt.

On iOS, add a specific, user-facing explanation to ios/Runner/Info.plist. This guide requests foreground access, so it uses the when-in-use key:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to show nearby collection points.</string>

Replace that sentence with the actual feature benefit. Vague wording can reduce trust and may cause store-review questions. Background tracking requires additional native configuration and a defensible product need; do not enable it for a foreground-only screen.

Treat service availability and app permission as separate gates

Geolocator.isLocationServiceEnabled asks whether the device-level location service is active. A user may grant your app permission while switching GPS or system location off, so checking permission alone is insufficient.

import 'package:geolocator/geolocator.dart';

Future<LocationPermission> obtainLocationPermission() async {
  final serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    throw StateError('Location services are switched off.');
  }

  var permission = await Geolocator.checkPermission();

  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
  }

  return permission;
}

bool canReadLocation(LocationPermission permission) {
  return permission == LocationPermission.whileInUse ||
      permission == LocationPermission.always;
}

Request only after explaining why the feature needs location, ideally in response to a tap. Repeatedly presenting a permission request during page construction gives the user no context and can quickly lead to permanent denial.

Fetch a single position with explicit accuracy settings

A one-shot fix suits check-ins, nearby searches, delivery-address confirmation, and forms that need a location once. Pass LocationSettings rather than silently accepting defaults, because accuracy affects both usefulness and power consumption.

import 'package:geolocator/geolocator.dart';

Future<Position> readCurrentPosition() async {
  final permission = await obtainLocationPermission();

  if (permission == LocationPermission.denied) {
    throw StateError('Location permission was denied.');
  }

  if (permission == LocationPermission.deniedForever) {
    throw StateError(
      'Location permission must be enabled in the app settings.',
    );
  }

  if (!canReadLocation(permission)) {
    throw StateError('Location permission is unavailable.');
  }

  const settings = LocationSettings(
    accuracy: LocationAccuracy.high,
    timeLimit: Duration(seconds: 15),
  );

  return Geolocator.getCurrentPosition(
    locationSettings: settings,
  );
}

LocationAccuracy.high is reasonable when nearby results depend on a useful fix, but it is unnecessary for coarse regional content. The timeout prevents an indefinite spinner inside a building or other weak-signal environment. A timeout is not proof that permission failed, so report it as an acquisition problem and allow another attempt.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Put the complete one-shot flow behind a clear action

The following screen keeps permission prompts away from initState, disables duplicate requests, and checks mounted after asynchronous work. It also displays the reported accuracy so that users are not misled by coordinates from a poor fix.

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

void main() {
  runApp(const MaterialApp(home: LocationPage()));
}

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

  @override
  State<LocationPage> createState() => _LocationPageState();
}

class _LocationPageState extends State<LocationPage> {
  Position? _position;
  String _status = 'No location requested.';
  bool _busy = false;

  Future<void> _locate() async {
    setState(() {
      _busy = true;
      _status = 'Checking location access…';
    });

    try {
      final serviceEnabled =
          await Geolocator.isLocationServiceEnabled();

      if (!serviceEnabled) {
        setState(() {
          _status = 'Switch on device location, then try again.';
        });
        return;
      }

      var permission = await Geolocator.checkPermission();

      if (permission == LocationPermission.denied) {
        permission = await Geolocator.requestPermission();
      }

      if (permission == LocationPermission.deniedForever) {
        setState(() {
          _status = 'Location is blocked in the app settings.';
        });
        return;
      }

      if (permission != LocationPermission.whileInUse &&
          permission != LocationPermission.always) {
        setState(() {
          _status = 'Location permission was not granted.';
        });
        return;
      }

      const settings = LocationSettings(
        accuracy: LocationAccuracy.high,
        timeLimit: Duration(seconds: 15),
      );

      final position = await Geolocator.getCurrentPosition(
        locationSettings: settings,
      );

      if (!mounted) return;

      setState(() {
        _position = position;
        _status = 'Location received.';
      });
    } on TimeoutException {
      if (mounted) {
        setState(() {
          _status = 'No position arrived in time. Try outdoors.';
        });
      }
    } catch (error) {
      if (mounted) {
        setState(() {
          _status = 'Could not read location: $error';
        });
      }
    } finally {
      if (mounted) {
        setState(() {
          _busy = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final position = _position;

    return Scaffold(
      appBar: AppBar(title: const Text('Current location')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(_status),
            const SizedBox(height: 16),
            if (position != null) ...[
              Text('Latitude: ${position.latitude}'),
              Text('Longitude: ${position.longitude}'),
              Text(
                'Accuracy: ${position.accuracy.toStringAsFixed(1)} m',
              ),
            ],
            const SizedBox(height: 24),
            FilledButton(
              onPressed: _busy ? null : _locate,
              child: Text(
                _busy ? 'Finding location…' : 'Get my location',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

TimeoutException is available through Dart's asynchronous library, which Flutter exports for this use. In a larger application, translate technical exceptions into stable domain states rather than showing raw exception text to users.

Respond properly when permission is denied forever

LocationPermission.deniedForever means another call to requestPermission cannot recover access. Present an explanation and let the user choose whether to open the operating-system settings; do not redirect them without a tap.

Future<void> openSettingsForPermanentDenial() async {
  final opened = await Geolocator.openAppSettings();

  if (!opened) {
    throw StateError('The operating system could not open app settings.');
  }
}

Future<void> openDeviceLocationSettings() async {
  final opened = await Geolocator.openLocationSettings();

  if (!opened) {
    throw StateError(
      'The operating system could not open location settings.',
    );
  }
}

These methods address different problems. openAppSettings lets the user alter this app's permission, whereas openLocationSettings targets the device service. After the user returns, call isLocationServiceEnabled and checkPermission again because opening settings does not guarantee that anything changed.

Follow movement with getPositionStream

Navigation, exercise tracking, and a moving map marker need updates rather than a single snapshot. A distance filter reduces noise and avoids rebuilding the interface for every tiny reported movement.

import 'dart:async';

import 'package:geolocator/geolocator.dart';

class LivePositionTracker {
  StreamSubscription<Position>? _subscription;

  Future<void> start({
    required void Function(Position position) onPosition,
    required void Function(Object error) onError,
  }) async {
    await stop();

    const settings = LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 10,
    );

    _subscription = Geolocator.getPositionStream(
      locationSettings: settings,
    ).listen(
      onPosition,
      onError: onError,
    );
  }

  Future<void> stop() async {
    await _subscription?.cancel();
    _subscription = null;
  }
}

Run the same service and permission checks before calling start. Own this tracker at the same lifecycle level as the feature: a page should stop it in dispose, while an intentional application-wide tracker needs an application-scoped owner. A foreground permission should not be treated as permission to continue collecting coordinates after the feature has ended.

Choose accuracy and update frequency for the actual feature

Higher accuracy can take longer, consume more power, and still produce an imprecise result when satellite visibility is poor. Inspect Position.accuracy, which expresses the estimated horizontal accuracy in metres, before accepting a fix for a sensitive operation.

  • Use lower accuracy for broad regional content where city-level results are adequate.
  • Use high accuracy for nearby-place searches, but show the accuracy and support retrying.
  • Increase distanceFilter when the interface does not need metre-by-metre updates.
  • Reject impossible coordinates or stale business actions at the domain layer, not merely in the widget.
  • Do not assume every position includes meaningful speed, heading, or altitude data.

A location reading identifies where the device believes it is; it does not prove the user's identity, presence at a venue, or ownership of the device. Server-side rules should not trust client coordinates as a sole security control.

Avoid the mistakes that make location screens unreliable

  • Requesting before checking the service: permission may be granted while device location remains disabled.
  • Treating deniedForever like denied: another permission request cannot display the system prompt; offer app settings instead.
  • Forgetting native declarations: Dart code cannot compensate for missing Android manifest permissions or the iOS usage description.
  • Starting a stream before access is granted: establish the service and permission state first, then subscribe.
  • Leaking the subscription: cancel live updates when their owner is disposed or tracking stops.
  • Demanding maximum accuracy everywhere: match precision and update frequency to the user-visible benefit.
  • Assuming a successful call is perfectly precise: inspect the accuracy value and handle weak fixes.
  • Calling setState after asynchronous work without checking mounted: the user may leave while the permission sheet or GPS request is open.

Also test repeated interactions. A robust screen behaves sensibly after denial, after a trip to settings, after rotating or leaving the page, and after tapping the location button more than once.

Verify each operating-system branch on a device

Automated widget tests cannot reproduce the native permission controller by themselves, so keep permission decisions behind a small service abstraction if you need deterministic UI tests. Regardless of test architecture, complete this device matrix before release:

  1. Install fresh, keep location services enabled, and grant permission. Confirm that coordinates and accuracy appear.
  2. Reinstall or reset permissions, deny the first request, and confirm that the screen remains usable.
  3. Choose permanent denial where the platform offers it. Confirm that the app offers an app-settings action rather than repeatedly requesting.
  4. Disable device location while app permission remains granted. Confirm that the service-specific message and location-settings route appear.
  5. Start live tracking, move with the device or simulate a route, and verify that the distance filter produces updates.
  6. Leave the tracking screen and confirm that no further UI updates or location indicator activity continue unexpectedly.
  7. Test a weak-signal environment and confirm that timeout or low-accuracy handling does not masquerade as permission denial.

Finally, exercise both Android and iOS. Their settings screens, approximate-location choices, and permission transitions differ, so a successful test on one platform is not evidence that the other platform is correctly configured.

Further reads

Keep going with these related tutorials from this site.