A runner starts a 10 km run, locks the phone, and puts it in an armband. Forty minutes later they look at the map and there’s a straight line cutting across a park, because the OS suspended the app for six minutes and you lost every point in between. Flutter background location tracking is mostly about not letting that happen, and the work is almost all on the native side, with different rules on iOS and Android.
We built this for Kompete, a running app with high-frequency GPS, live position broadcasting, per-km splits and live spectator maps. Kompete is Flutter with platform channels for background location. Here’s what each platform needs and where the battery goes.
Flutter background location tracking on Android: a foreground service
On Android, the reliable way to keep receiving location while the screen is off is a foreground service with the location type. A foreground service shows a persistent notification (“Run in progress, 4.2 km”), which is honest with the user and is what keeps the system from killing you.
What the current Android documentation requires:
- Declare the service with
android:foregroundServiceType='location'and request theFOREGROUND_SERVICE_LOCATIONpermission (required from Android 14, API 34). - Before starting it, the user must have location services on and your app must hold
ACCESS_FINE_LOCATIONorACCESS_COARSE_LOCATION. - Pass
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATIONtostartForeground(). - Declare your foreground service types in Play Console, under App content.
The rule that decides your permission strategy: location permissions are subject to while-in-use restrictions, so you can’t start a location foreground service while the app is in the background unless you hold ACCESS_BACKGROUND_LOCATION. For a running app that’s good news. The user taps Start while the app is open, you start the service right then, and it keeps running after they lock the phone. You don’t need the background location permission at all, and you avoid the extra Play review that comes with it.
Inside the service, request updates from the fused location provider with high accuracy and an interval that fits the activity (around one second for running). From Android 13, notification permission is a runtime prompt too. Ask for it before the first run so the user can see the run notification.
The OEM problem
Some Android manufacturers add their own battery managers on top of stock Android, and they can stop background work that stock Android would allow. A foreground service survives most of them, but not all. On those devices, a short in-app screen that explains the setting and links to the system battery settings saves a lot of “my run got cut off” support emails. Test on the brands your users actually own, not only Pixels.
iOS: background modes and the blue indicator
iOS has no foreground services. Instead:
- Turn on the Location updates background mode in Signing & Capabilities. This adds
locationtoUIBackgroundModes. - Start updates while the app is in the foreground, after the user taps Start. With the classic
CLLocationManager, setallowsBackgroundLocationUpdates = true. On iOS 17 and later you can useCLLocationUpdate.liveUpdates()and hold aCLBackgroundActivitySessionfor the length of the run. - Expect the system’s location indicator in the status bar while you track. That’s the trade for being allowed to keep running.
“When In Use” authorization is enough for this, as long as tracking starts in the foreground. Don’t ask for “Always” unless you have a feature that needs to start tracking with the app closed. Users decline it, and reviewers ask why you need it.
Settings that matter for a run on the classic API: activityType = .fitness, desiredAccuracy = kCLLocationAccuracyBest, and pausesLocationUpdatesAutomatically = false. The automatic pause is meant to save battery when the user stops moving, and it also fires at a long traffic light, where it’s exactly wrong for a runner.
If iOS terminates the app during a session, Apple’s guidance is to recreate your session objects immediately when the app is relaunched in the background, and queued updates will then be delivered. Persist “a run is in progress” to disk at the start so the relaunch code knows to do that.
Getting the points from native to Dart
Use an EventChannel: native code pushes each location as a small map (latitude, longitude, accuracy, speed, timestamp) and Dart listens to a stream. Two rules:
- Write every point to local storage on the native side first. The Flutter engine can be paused or torn down while the native service keeps running. If Dart is the only place points are kept, you’ll lose them when the engine stops. Native writes, Dart reads.
- Filter before you draw. Drop points with poor horizontal accuracy, and points that imply an impossible speed from the last good one. Raw GPS in a city has jumps of tens of metres off tall buildings, and those spikes inflate distance and wreck split times.
Where the battery goes
Keeping GPS on is the cost, and for an app like this you can’t avoid it. Everything else is optional. Don’t open a network request per point: batch uploads every few seconds, or stream over one long-lived connection if the app shows live positions. Don’t redraw the map while the screen is off. Keep per-point work small, because it runs thousands of times a run. And stop everything the instant the run ends. Leaving location on after a run is the bug users notice in their battery settings.
If your product needs live location (a fitness app, a delivery app, a field-staff tool), orithLabs has shipped it on both platforms, in Flutter and with native code where it counts. Have a look at our mobile development work or the Kompete case study.