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:
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.
Let’s do a quick check of things we need before jumping right ahead:
That’s pretty much it!
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.
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"/>
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.
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 grantedPermissionStatus.denied
: the permission for location services has been deniedPermissionStatus.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 caseIf 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.
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.
Add the dependency to your pubspec.yaml
file:
dependencies: geocode: 1.0.1
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}"; } }
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:
requestPermisssions()
will not show the native prompt asking for location permissions. Make sure to handle this edge caseBecause 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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ 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>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]