Working with Location Services in Flutter

Location services are a crucial component of many modern mobile applications. Whether it’s for mapping, geofencing, or providing location-aware content, accurate and reliable location data can significantly enhance user experience. Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, makes integrating location services relatively straightforward. This article will guide you through the process of working with location services in Flutter, providing comprehensive explanations and code examples.

Why Use Location Services?

Integrating location services into your Flutter app opens a wide array of possibilities:

  • Mapping and Navigation: Displaying maps and providing navigation guidance.
  • Geofencing: Triggering actions when users enter or exit specific geographic areas.
  • Location-Aware Content: Delivering content tailored to the user’s current location.
  • Tracking: Monitoring user movements for various purposes like fitness tracking or delivery services.

Setting Up Your Flutter Project for Location Services

Step 1: Adding Dependencies

To work with location services in Flutter, you need to add the geolocator package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  geolocator: ^10.1.0

After adding the dependency, run flutter pub get in your terminal to fetch the package.

Step 2: Configuring Permissions

Before accessing the user’s location, you need to request the necessary permissions. The process varies slightly depending on the platform (Android or iOS).

Android Configuration

Open your android/app/src/main/AndroidManifest.xml file and add the following permissions:


    
    
    
    
    
    
    
        
    

Replace your_package_name and your_app_name with your app’s actual package and name, respectively.

Starting with Android 11 (API level 30), you also need to add the QUERY_ALL_PACKAGES permission if you target a higher API level to check whether location services are enabled on the device:

iOS Configuration

Open your ios/Runner/Info.plist file and add the following keys to request location permissions:


    
    NSLocationWhenInUseUsageDescription
    This app needs to access your location when in use to provide location-based services.
    NSLocationAlwaysUsageDescription
    This app needs to access your location even when in the background to enable geofencing features.
    NSLocationAlwaysAndWhenInUseUsageDescription
    This app needs to access your location at all times to provide continuous location-based services.

Update the description strings with clear and informative messages explaining why your app needs location permissions.

Implementing Location Services in Flutter

Step 1: Importing the geolocator Package

In your Dart file, import the geolocator package:

import 'package:geolocator/geolocator.dart';

Step 2: Checking Location Service and Permission Status

Before retrieving location data, it’s essential to check whether location services are enabled and if the app has the necessary permissions:

import 'package:geolocator/geolocator.dart';

Future checkLocationPermission() async {
  bool serviceEnabled;
  LocationPermission permission;

  // Test if location services are enabled.
  serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    // Location services are not enabled don't continue
    // accessing the position and request users of the 
    // App to enable the location services.
    return false;
  }

  permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      // Permissions are denied, next time you could try
      // requesting permissions again (this is also where
      // Android's shouldShowRequestPermissionRationale 
      // returned true. According to Android guidelines
      // your App should show an explanatory UI now.
      return false;
    }
  }
  
  if (permission == LocationPermission.deniedForever) {
    // Permissions are denied forever, handle appropriately. 
    return false;
  }

  // When we reach here, permissions are granted and we can
  // continue accessing the position of the device.
  return true;
}

Step 3: Getting the Current Location

To retrieve the user’s current location, use the getCurrentPosition method:

Future getCurrentLocation() async {
  final hasPermission = await checkLocationPermission();

  if (!hasPermission) {
    return null;
  }

  try {
    return await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);
  } catch (e) {
    debugPrint('Error getting location: $e');
    return null;
  }
}

The desiredAccuracy parameter allows you to specify the desired accuracy level for the location data. Higher accuracy levels may consume more battery.

Step 4: Continuously Listening for Location Updates

To listen for continuous location updates, use the getPositionStream method:

StreamSubscription? positionStream;

void startListeningForLocation() async {
  final hasPermission = await checkLocationPermission();
    
  if (!hasPermission) {
    return;
  }
  
  const LocationSettings locationSettings = LocationSettings(
    accuracy: LocationAccuracy.high,
    distanceFilter: 100,
  );

  positionStream = Geolocator.getPositionStream(locationSettings: locationSettings).listen(
          (Position? position) {
        if (position != null) {
          print('Latitude: ${position.latitude}, Longitude: ${position.longitude}');
        }
      });
}

void stopListeningForLocation() {
  positionStream?.cancel();
}

The distanceFilter parameter specifies the minimum distance (in meters) that the device must move before a new location update is emitted.

Handling Errors and Edge Cases

When working with location services, it’s essential to handle potential errors and edge cases gracefully:

  • Location Services Disabled: Prompt the user to enable location services in their device settings.
  • Permissions Denied: Explain why the app needs location permissions and guide the user to grant them in the app settings.
  • No GPS Signal: Provide alternative solutions, such as using Wi-Fi or cell tower triangulation, to estimate the user’s location.
  • Timeouts: Implement timeout mechanisms to prevent the app from waiting indefinitely for location data.

Complete Example

import 'dart:async';

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Location Services',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Location Services'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  Position? _currentPosition;
  StreamSubscription? positionStream;

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

  Future _checkLocationPermission() async {
    bool serviceEnabled;
    LocationPermission permission;

    // Test if location services are enabled.
    serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      // Location services are not enabled don't continue
      // accessing the position and request users of the 
      // App to enable the location services.
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
          content: Text(
              'Location services are disabled. Please enable the services')));
      return false;
    }

    permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        // Permissions are denied, next time you could try
        // requesting permissions again (this is also where
        // Android's shouldShowRequestPermissionRationale 
        // returned true. According to Android guidelines
        // your App should show an explanatory UI now.
        ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Location permissions are denied')));
        return false;
      }
    }

    if (permission == LocationPermission.deniedForever) {
      // Permissions are denied forever, handle appropriately.
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
          content: Text(
              'Location permissions are permanently denied, we cannot request permissions.')));
      return false;
    }

    // When we reach here, permissions are granted and we can
    // continue accessing the position of the device.
    return true;
  }

  Future _getCurrentLocation() async {
    final hasPermission = await _checkLocationPermission();

    if (!hasPermission) {
      return;
    }

    try {
      final position = await Geolocator.getCurrentPosition(
          desiredAccuracy: LocationAccuracy.high);
      setState(() {
        _currentPosition = position;
      });
    } catch (e) {
      debugPrint('Error getting location: $e');
      ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error getting location: $e')));
    }
  }

  void _startListeningForLocation() async {
    final hasPermission = await _checkLocationPermission();

    if (!hasPermission) {
      return;
    }

    const LocationSettings locationSettings = LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 100,
    );

    positionStream =
        Geolocator.getPositionStream(locationSettings: locationSettings).listen(
            (Position? position) {
          if (position != null) {
            setState(() {
              _currentPosition = position;
            });
            print(
                'Latitude: ${position.latitude}, Longitude: ${position.longitude}');
          }
        });
  }

  void _stopListeningForLocation() {
    positionStream?.cancel();
  }

  @override
  void dispose() {
    _stopListeningForLocation();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_currentPosition != null)
              Text(
                  'Latitude: ${_currentPosition!.latitude}, Longitude: ${_currentPosition!.longitude}')
            else
              const Text('Getting current location...'),
            ElevatedButton(
                onPressed: _getCurrentLocation,
                child: const Text("Get Current Location")),
            ElevatedButton(
                onPressed: _startListeningForLocation,
                child: const Text("Start Listening for Location Updates")),
            ElevatedButton(
                onPressed: _stopListeningForLocation,
                child: const Text("Stop Listening for Location Updates")),
          ],
        ),
      ),
    );
  }
}

Conclusion

Integrating location services into your Flutter application can greatly enhance its functionality and user experience. By using the geolocator package, you can easily retrieve the user’s current location, listen for continuous location updates, and implement location-aware features. Remember to handle permissions and potential errors gracefully to ensure a seamless experience for your users. Properly managing and utilizing location services can unlock a wide range of possibilities for your Flutter apps.