Bhavya Mishra Bhavya wanted to be a writer, so she writes code now. Bhavya’s career has been focused on all things Android for the past five years. Drawn to Fueled by the team culture and the cutting edge technologies we use, she doesn’t plan on stopping anytime soon!

Geolocation and geocoding in Flutter

5 min read 1578

Flutter geolocations and geolocator

Discovering user location is a very common and powerful use case for mobile applications these days. If you have ever tried to implement locations in Android, you know how complex and confusing the boilerplate code can get.

This is not the same with Flutter though — it has a lot of amazing packages that abstract that boilerplate code out for you and makes implementing geolocations a dream. Another bright side is that you get the functionality on both Android and iOS.

Let’s take a quick look at what we are building today to gather location data:

Flutter geolocation demo

This article walks you through two of the most popular and easy-to-use Flutter packages for geolocation.

Let’s start with location, a Flutter favorite package. This one is as simple as it gets. Just three simple steps and you will be able to get the current user location along with handling location permissions.

Prerequisites

Let’s do a quick check of things we need before jumping right ahead:

  • The Flutter SDK
  • An editor; you can use either Visual Code or Android Studio
  • At least beginner-level knowledge of Flutter

That’s pretty much it!

Using the Flutter location package

Set up

Add the dependency to your pubspec.yaml file:

dependencies:
    location: ^4.2.0

Since permissions are handled differently by Android and iOS, we will have to add them separately on each platform.

For Android

Add the following location permissions to AndroidManifest.xml:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

If you want to access the user’s location in the background as well, use the enableBackgroundMode({bool enable}) API before accessing location in the background and add background permission to the manifest file as well:

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

For iOS

Add the following location permissions to Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs to access your location</string>

NSLocationWhenInUseUsageDescription is the only permission you need. This allows you to access background location as well, the only caveat being that a blue badge is shown in the status bar when the app accesses location while in the background. Unlike Android, where we added separate permission for accessing the user’s location in the background.

Location permissions

We need to check Location Service status and Permission status before requesting user location, which is easily done using these few lines of code:

Location location = new Location();

bool _serviceEnabled;
PermissionStatus _permissionGranted;

_serviceEnabled = await location.serviceEnabled();
if (!_serviceEnabled) {
 _serviceEnabled = await location.requestService();
 if (!_serviceEnabled) {
   return null;
 }
}

_permissionGranted = await location.hasPermission();
if (_permissionGranted == PermissionStatus.denied) {
 _permissionGranted = await location.requestPermission();
 if (_permissionGranted != PermissionStatus.granted) {
   return null;
 }
}

First, we create a Location() object that is provided by the location package, which in turn provides us with two useful methods. serviceEnabled() checks if the device location is enabled or the user has disabled it manually.

For the latter, we display a native prompt that allows users to enable location quickly by calling requestService() and then we check one more time if they then enabled it from the prompt.

Once we are sure that location service is enabled, the next step is to check if our app has the necessary permissions to use it by calling hasPermission() that, which returns the PermissionStatus.

PermissionStatus is an enum that can have one of these three values:

  • PermissionStatus.granted: the permission for location services has been granted
  • PermissionStatus.denied: the permission for location services has been denied
  • PermissionStatus.deniedForever: the permission for location services has been denied forever by the user. This applies only to iOS. No dialog will be displayed on requestPermission() in this case

If the status is denied, we can display the system prompt requesting location permissions by calling requestPermission(). For status granted, we can access location right away, so we return a null.



Use location.enableBackgroundMode(enable: true) if you want to access user location in the background as well.

Getting current location

If location services are available and the user has granted location permission, then we can get user location with just two lines of code — nope, I am not kidding:

LocationData _locationData;
_locationData = await location.getLocation();

The LocationData class provides the following location info:

class LocationData {
  final double latitude; // Latitude, in degrees
  final double longitude; // Longitude, in degrees
  final double accuracy; // Estimated horizontal accuracy of this location, radial, in meters
  final double altitude; // In meters above the WGS 84 reference ellipsoid
  final double speed; // In meters/second
  final double speedAccuracy; // In meters/second, always 0 on iOS
  final double heading; // Heading is the horizontal direction of travel of this device, in degrees
  final double time; // timestamp of the LocationData
  final bool isMock; // Is the location currently mocked
}

You can also get continuous callbacks by adding the onLocationChanged listener to listen to the location updates when the user location is changing, a very good use case for taxi apps, driver/rider apps, and so on:

location.onLocationChanged.listen((LocationData currentLocation) {
  // current user location
});

N.B., don’t forget to cancel the stream subscription once you want to stop listening to the updates.

Voila! Now we have the current latitude and longitude values for user location.

Let’s utilize these latitude and longitude values to fetch the user’s complete address or reverse geocode.

For this, we will use another amazing Flutter package: geocode.

Using the Flutter geocode package

Set up

Add the dependency to your pubspec.yaml file:

dependencies:
    geocode: 1.0.1

Getting an address

Getting an address couldn’t be easier. Just call reverseGeocoding(latitude: lat, longitude: lang). That’s it! A complete function with null checks looks like this:

Future<String> _getAddress(double? lat, double? lang) async {
 if (lat == null || lang == null) return "";
 GeoCode geoCode = GeoCode();
 Address address =
     await geoCode.reverseGeocoding(latitude: lat, longitude: lang);
 return "${address.streetAddress}, ${address.city}, ${address.countryName}, ${address.postal}";
}

Wasn’t that simple!

The complete code looks like this:

class GetUserLocation extends StatefulWidget {
 GetUserLocation({Key? key, required this.title}) : super(key: key);
 final String title;

 @override
 _GetUserLocationState createState() => _GetUserLocationState();
}

class _GetUserLocationState extends State<GetUserLocation> {
 LocationData? currentLocation;
 String address = "";

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(),
     body: Center(
       child: Padding(
         padding: EdgeInsets.all(16.0),
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: <Widget>[
             if (currentLocation != null)
               Text(
                   "Location: ${currentLocation?.latitude}, ${currentLocation?.longitude}"),
             if (currentLocation != null) Text("Address: $address"),
             MaterialButton(
               onPressed: () {
                 _getLocation().then((value) {
                   LocationData? location = value;
                   _getAddress(location?.latitude, location?.longitude)
                       .then((value) {
                     setState(() {
                       currentLocation = location;
                       address = value;
                     });
                   });
                 });
               },
               color: Colors.purple,
               child: Text(
                 "Get Location",
                 style: TextStyle(color: Colors.white),
               ),
             ),
           ],
         ),
       ),
     ),
   );
 }

 Future<LocationData?> _getLocation() async {
   Location location = new Location();
   LocationData _locationData;

   bool _serviceEnabled;
   PermissionStatus _permissionGranted;

   _serviceEnabled = await location.serviceEnabled();
   if (!_serviceEnabled) {
     _serviceEnabled = await location.requestService();
     if (!_serviceEnabled) {
       return null;
     }
   }

   _permissionGranted = await location.hasPermission();
   if (_permissionGranted == PermissionStatus.denied) {
     _permissionGranted = await location.requestPermission();
     if (_permissionGranted != PermissionStatus.granted) {
       return null;
     }
   }


   _locationData = await location.getLocation();

   return _locationData;
 }

 Future<String> _getAddress(double? lat, double? lang) async {
   if (lat == null || lang == null) return "";
   GeoCode geoCode = GeoCode();
   Address address =
       await geoCode.reverseGeocoding(latitude: lat, longitude: lang);
   return "${address.streetAddress}, ${address.city}, ${address.countryName}, ${address.postal}";
 }
}

Common pitfalls

Even though these packages have made our lives easier and we don’t have to deal with the complex process of accessing locations natively in Android and iOS, there are quite a few issues that you might face. Let’s take a look at them and the steps that can help you remediate these issues:

  • App leaking memory: If you are listening to location updates continuously, make sure to cancel the stream subscription, once you want to stop listening to the updates
  • The user has to accept the location permission to always allow to use the background location. The Android 11 option to always allow is not presented on the location permission dialog prompt. The user has to enable it manually from the app settings
  • The user might have denied location forever on iOS so requestPermisssions() will not show the native prompt asking for location permissions. Make sure to handle this edge case
  • User might revoke location permissions from the app settings at any time, so make sure to check for them when the app resumes before accessing location data

Conclusion

Because accessing locations has been simplified by Flutter, it might be tempting for us as devs to add it to our app right away. But at the same time, we need to make sure that our app actually fits the use case for requesting user location and utilizing it to add some value for the user, rather than just sending location data to the server.

With the increase in security and privacy in the upcoming OS versions for both Android and iOS, accessing location data without providing a value to the user might get your app rejected from the stores. There are a lot of good use cases where you can use user location, e.g., personalizing home screen based on user location for a food/delivery app that displays restaurants ordered by proximity to the user’s current location. A pickup/delivery app is the most common use case.

You can also ask for user location on a specific screen where you actually want to use it, rather than asking it right away on the home screen. This makes it clearer to the user, and they are less likely to deny location permissions.

Thanks for sticking around, happy coding fella! You can access the sample app used in the article here on GitHub.

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Bhavya Mishra Bhavya wanted to be a writer, so she writes code now. Bhavya’s career has been focused on all things Android for the past five years. Drawn to Fueled by the team culture and the cutting edge technologies we use, she doesn’t plan on stopping anytime soon!

Leave a Reply