How to fetch user location in background with Flutter

Pierre Sabot
11 min readMay 6, 2020

Disclaimer: This is my first post on Medium (and my first post on any platform for that matter). Also, English is not my primary language, so I apologize for any mistake you are about to spot. I’ll gladly edit the article based on your feedbacks.

I’m pretty new to the mobile ecosystem (I’m a web developer) and I wanted to give Flutter a shot since it looked so promising (the fact I would not need to learn any mobile specific development was really appealing. SPOILER: I had to look at all kinds of mobile concepts).

I wanted to create an app that would track my position once in a while and store it into a database. It’s basically a travel tracker, so my family and friends could browse those locations on a map to follow my journey. (Yeah, I was planning to make a world trip… Needless to say it was planned way before that COVID thing. Anyway, more time to finish the app I guess 😐). Among other things, one of the core functionality is the ability to fetch a device position even when the app is not active. I don’t plan on opening the app each time I need my location to be recorded.

This article’s main focus will be:

  • How to fetch a user location
  • How to fetch it even if the app is running in the background
  • The limitations of the system
  • Other alternatives that exist

There are few resources to get those things done but Flutter is still pretty new and u̵n̵s̵t̵a̵b̵l̵e̵ changing so I thought I would make my own article that shows what worked for me. Hope this’ll help.

For this tutorial, I just started from a blank projet, generated by flutter.

Your app should look like that:

The default app from flutter

I’ll only show the Android configuration through this article for clarity’s sake but all iOS specifics are documented on the plugins’ pages.

How to fetch a user location

The first obvious thing to do is to get the device position, so we can have the coordinates we need.

This step is pretty simple. The only thing you need is the geolocator (https://pub.dev/packages/geolocator) plugin.

Add the dependency to your pubspec.yaml

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
geolocator: ^5.3.0

You also need to add two permissions to get it to work. To do that in Android, just add one of the two following lines to your AndroidManifest.xml located in your_project/android/app/src/main/

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

The first permission allows to get a precise location while the second one would be to get an approximated location.

So, why should you use an approximated location when you can have a precise one?

ACCESS_COARSE_LOCATION gives you the last-known location which is battery friendly but it has a dependency to Google Play Services whereas ACCESS_FINE_LOCATION does not have this dependency, but is more battery draining. I’ll use ACCESS_FINE_LOCATION for this tutorial, so I won’t need Google Play Services.

Finally, with very few modifications to your main.dart , you will have something like that.

There are only two lines worth mentioning:

GeolocationStatus geolocationStatus = await Geolocator().checkGeolocationPermissionStatus();

Checks whether the user has given his permission to be located and if not, asks him to do so.

Position userLocation = await geoLocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);

As you have guessed, this line will actually get the current position. As explained before, I chose to go with the ACCESS_FINE_LOCATION permission, so I can set my desiredAccuracy to high.

If you read the code further down, you will see that I only set the position state variable, which will be displayed in the Text widget.

You can now run your app! If everything worked well, you should see the following:

Blurred position so you won’t knock at my door if the code does not work

That’s great, we’re half way there!

Fetch the location in background

Unfortunately, this plugin does not provide a way to get the location when the app is not in foreground.

We will have to find a way to execute dart code in the background to run the same function than before, even when the app is idle.

This is when WorkManager (https://pub.dev/packages/workmanager) come in pretty handy!

The plugin’s creators, Tim Rijckaert and Jeremie Vincke wrote an article (https://medium.com/vrt-digital-studio/flutter-workmanager-81e0cfbd6f6e) on how to use this plugin in-depth. You should read it as well if you want to learn more about WorkManager.

As the documentation states:

Flutter WorkManager is a wrapper around Android’s WorkManager and iOS’ performFetchWithCompletionHandler, effectively enabling headless execution of Dart code in the background.

Let’s try it, so we can run our previous code in the background!

First things first, let’s add our new dependency in our pubspec.yaml file.

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
geolocator: ^5.3.0
workmanager: ^0.2.0

To get this to work, we need to define a top-level (or a static) function that will execute our background task.

...
import 'package:workmanager/workmanager.dart';

const fetchBackground = "fetchBackground";

void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
switch (task) {
case fetchBackground:
Position userLocation = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
break;
}
return Future.value(true);
});
}

This is exactly the same code that fetched the position earlier, it’s now just wrapped in the callback that will be called in the background. So, we have defined a new task, but we need to register it somehow so it will be called at a given frequency.

To do that, let’s edit our initState function:

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

// We don't need it anymore since it will be executed in background
//this._getUserPosition();

Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);

Workmanager.registerPeriodicTask(
"1",
fetchBackground,
frequency: Duration(minutes: 30),
);

}

There are only two steps to make it work:

  • We need to initialize the workmanager by providing a callback function, which is the top-level function callbackDispatcher that we wrote above. I added the isInDebugMode flag which will notify us when any task is running and will show whether the job has succeeded or failed.
  • Second thing to do is to actually register our task. There are several kinds of tasks, but in our case we want a periodicTask which will run indefinitely every X minutes, depending on what we will define in the frequency value. I chose to run the task every 30 minutes but you can define your own frequency here.
    The first parameter is a unique name which is required because it will act as the task identifier. Then, we can cancel this task by its name if needed.
    The second parameter is the task name that will be called. In our case, it is set to fetchBackground which is the constant we defined earlier ( const fetchBackground = “fetchBackground";) , above our top-level function.

The minimum possible frequency for Android is 15 minutes. Even if you set a lower number, it will be overridden by Android. The frequency customisation is only available for Android. For iOS you will need to set it differentl : https://github.com/vrtdev/flutter_workmanager/blob/master/IOS_SETUP.md#enabling-background-fetch

That’s all we need to execute our task in the background!

Digging a bit deeper

I tried to understand how the flutter plugin works. It’s a bit more complex than my following explanations, but here’s what I’ve gathered.
Under the hood, the flutter plugin does a lot of things:

  1. The callback we defined has to be Dart code inside a static or a top-level function. This function will be invoked when the background task runs.
  2. Dart can’t run on its own and needs to be powered by Flutter’s engine. To do so, the plugin needs to run internally the PluginUtilities.getCallbackHandle function to register the callbackDispatcher alongside with Flutter’s engine.
  3. Then, this will be sent to native over a MethodChannel, so Android can save it and use it later on (when the background task will run).
  4. We can now register our task via the flutter plugin, which will set an Android WorkManager task with our callback’s reference.
  5. When the Android WorkManager will execute our task with the saved Dart callback, it will run on an Isolated Dart execution context.

Bonus: get notified with the location

Everything seems to work but don’t you think it’s a bit frustrating that the fetched location won’t show up on your screen?

You know for sure that your task successfully ran but you can’t really tell if it fetched the correct location. All you know is you did not have any error.

On my app, I send those coordinates to an API which will save the position, but I thought it would be cool to have visual proof that our location has been fetched.

As always, there is a great plugin to fit that need!

This time, we’ll be using the flutter_local_notification package (https://pub.dev/packages/flutter_local_notifications).

It does a great deal of things, but we don’t really need that much here. We only want a basic notification whenever the background task fetched our position.

I won’t go into too much details about this since it’s not really our main focus here, but feel free to read the documentation as it contains many examples.

Create a new file named notification.dart in the lib folder (at the same level than your main.dart ) and copy/paste this code

This class contains a constructor which initializes the notification plugin with minimal configuration.
Then, we created a showNotificationWithoutSound function which will take a position and print it in the notification body.

This class is all we need, we can now instantiate it and call our function from our background task once we have the position fetched. Just edit the main.dart file so it matches this code:

import 'package:background_location/notification.dart' as notif;const fetchBackground = "fetchBackground";void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
switch (task) {
case fetchBackground:
Position userLocation = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
notif.Notification notification = new notif.Notification();
notification.showNotificationWithoutSound(userLocation);

break;
}
return Future.value(true);
});
}

And now, if you run your app once again, you should see the notification with the fetched location! ✔️

That’s it! You can now retrieve a device position in the background 👌

I hope this small tutorial will help you save some time. Feel free to ask any question if you think I was not clear enough.

The limitations of the system

After playing a bit with this background system, I noticed a few things that was not working as I expected.

The first thing I noticed is that the background tasks won’t run precisely at the frequency I defined in my code. Sometimes, they’d even be performed once per hour whereas I set them to run every 15 minutes. This is a not a bug but rather how WorkManager has been designed. WorkManager respects OS background restrictions and tries to run your work in a battery efficient way. This means that even if you set a frequency, it’s the OS that will ultimately decide when to run your background task. The only thing you’re sure of is that your task will be executed. You just don’t really know when to expect it. To be honest, I was expecting this because I read it in the documentation but I didn’t think the periodic interval would be so inconsistent. Anyway, it was not really an issue for me since I don’t need my position to be fetched at a specific time.
If you need to schedule your task at a precise frequency though, you should look at AlarmManager which respects your frequency but I’m afraid it won’t work on iOS anyway (or at least will be tricky).

The second and the real pain here are manufacturer’s limitations.

When I fetched my location in the background for the first time, I was really happy with the result and after some testing, I went on with the next thing on my to do list and pretty much forgot about all this.

This is only some time after that day that I discovered my task was not executed anymore. At all. The last task execution was successful, I did not kill my app, I did not restart my phone, I did not do anything.

At first, I thought that the flutter plugin wasn’t supporting the doze or idle mode and that was the cause of the issue. After some digging and desperations, I found out that I had bought the worst possible cellphone, background tasks wise. Indeed, I own a Xiami phone (9T) that runs on the EMUI OS. EMUI has a pretty drastic way of thinking and will gladly kill apps that are not in the foreground whenever it sees fit. It seems that they whitelist some of the more popular apps to avoid users’ complains but it’s a real issue for smaller creators.
This is not EMUI specific and you’ll find similar behavior with some other manufacturers.

To whitelist your app by hand on your phone, you will have to change your settings. The terms change from one manufacturer to another but the two mains parameters to change are the same:

  • autostart (or restart) option: it is disabled by default, and you’ll need to enable it if you want your app (and therefore your background task) to run after a phone reboot.
  • Power saving setting: you’ll likely need to disable any setting that refers to power saving.

ACR app (which has millions of downloads) has the same issue and provided some screenshots to demonstrate how to change your settings, based on your manufacturer.

It’s really inconvenient, and you will have to warn your audience that they should change their settings if they want your app to run flawlessly.

I don’t know if it’s possible in flutter yet but I saw on stackoverflow that you can create an intent to ask the user to change those settings programmatically. Maybe it’s possible to do something from this in flutter but I’m way too inexperienced to make something out of it. Happy to have your input in the comments section if it’s something you’re familiar with.

Otherwise, it seems that you can “wake up” your app with a foreground notification (FCM or JobScheduler for instance) but I don’t know if it bypasses the issue mentioned earlier.

Some alternatives

To offer some perspectives, I found two other ways to achieve pretty much the same thing.

Let me know if you’re aware of any other available options that might be out there!

Hope this article was useful to you and don’t hesitate to drop me a line if you have any insights on the limitations I mentioned or if you think of any improvement I should add!

You can find the code I used for this article on my github.

--

--

Pierre Sabot

I’m a full stack developer as they say. I can do anything. It will just be painfully bad.