Background Location in a Rider App: The Permission Is the Easy Half
The stream stops when the screen locks and nothing throws. The foreground service type Android 14 wants, the flag iOS wants, and the form a human at Play reads before any of it ships.
The rider app streams location perfectly. You run it on your desk, watch the marker crawl across the map, watch the customer app follow it, and hand a phone to your first rider. Then the support messages start: the map showed the rider parked outside the restaurant for eleven minutes and then jumping three streets in one frame.
A location stream is a foreground feature until you tell three separate systems otherwise. Android stops delivering updates when your process leaves the foreground, iOS suspends the process outright, and Google Play decides whether you are permitted to fix either. None of the three raises an error when it stops you.
Where those pings should be written, and what they cost per thousand orders, is the running-costs post; which rider should get the order is the dispatch one. This is the layer underneath both — keeping the stream alive at all, on two operating systems and one review queue.
Nothing throws when it stops
Android's location permissions are while-in-use permissions. The grant covers the foreground, and when your app leaves it the updates simply stop arriving: the subscription is still open, the callback is still attached, and nothing is delivered to it. iOS is blunter — the process is suspended, so there is nothing running to call back into.
Neither produces an exception, a failed request or a crash report. The symptom is a stale marker, which looks like a network problem, a Firebase problem or a map problem, and gets debugged as all three. It also reproduces only on a locked phone in somebody's pocket, which is the one state you never test in while you are holding the device and watching the screen.
Android: a service, a type, and a permission that never prompts
The mechanism that keeps location flowing is a foreground service, and once your app targets Android 14 (API level 34) a foreground service has to declare what kind of work it does. Each type has a matching permission, and location work needs FOREGROUND_SERVICE_LOCATION in addition to FOREGROUND_SERVICE, plus the type on the service element itself. An app targeting 34 or higher that misses either one gets a SecurityException the moment the service starts.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- You declare the permissions. The service element arrives
from the location plugin's own manifest and merges into
yours, so this is what the merged result has to look like
rather than something to paste in twice. When
startForeground throws, the merged manifest viewer in
Android Studio is where the answer is. -->
<service
android:name="com.baseflow.geolocator.GeolocatorLocationService"
android:foregroundServiceType="location"
android:exported="false" />FOREGROUND_SERVICE_LOCATION is a normal permission. It is granted at install and never shows a dialog, so it is easy to read the absence of a prompt as the absence of a requirement. It is a declaration to the system and to Play, not a request to the user. The dialog the user actually sees is still ACCESS_FINE_LOCATION.
Then there is the restriction that catches nearly everyone. You cannot start a location foreground service while your app is in the background unless ACCESS_BACKGROUND_LOCATION has already been granted — and starting tracking when the assignment push arrives is exactly the design most people reach for. On a phone in a pocket it does nothing, quietly. Start the service from a foreground action instead: the rider tapping Go on shift, with the app open in front of them.
"Allow all the time" is a second journey, in Settings
From Android 10 the order is fixed: you have to hold foreground location before you can ask for background location, and asking for background on its own — or for both in one go — is ignored by the system rather than refused. There is no error to notice. From Android 11 the second request does not open a dialog with a third option on it either; the system sends the user to a settings page, because Allow all the time is not offered in the in-app prompt at all.
permission_handler models that as two separate permissions, which makes the required order visible in the code:
Future<bool> requestShiftTracking() async {
// Foreground first, always. Requesting locationAlways directly,
// or both at once, is ignored on Android 10 and above.
final foreground = await Permission.locationWhenInUse.request();
if (!foreground.isGranted) return false;
final background = await Permission.locationAlways.request();
if (background.isGranted) return true;
// Everything else ends up in Settings, which is a screen the
// rider will not find on their own.
await openAppSettings();
return false;
}The screen in front of that call is doing more work than the call. A rider who is sent to a settings page with no explanation denies the permission, and a rider app without background location is a rider app that reports a position only while somebody is staring at it.
The Flutter side is one settings object
With the manifest right, the platform difference collapses into which settings class you hand to the stream.
final settings = Platform.isAndroid
? AndroidSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 25,
intervalDuration: const Duration(seconds: 10),
foregroundNotificationConfig: const ForegroundNotificationConfig(
notificationTitle: 'On shift',
notificationText: 'Sharing your location with the dispatcher',
enableWakeLock: true,
setOngoing: true,
),
)
: AppleSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 25,
// Maps to allowsBackgroundLocationUpdates. The Info.plist key
// alone does not switch background delivery on.
allowBackgroundLocationUpdates: true,
showBackgroundLocationIndicator: true,
pauseLocationUpdatesAutomatically: false,
);
_subscription = Geolocator.getPositionStream(locationSettings: settings)
.listen(_onFix);distanceFilter is the single largest battery lever in the file. At zero you get every fix the hardware produces, which is a flat battery before the end of a shift and a rider who turns the app off. Twenty-five metres is a marker that still moves smoothly on a map.
Note that subscribing and writing are separate decisions. A stream at twenty-five metres still emits far more often than a database needs, and pushing every emission straight into Firestore is the line item that turns a cheap app into an expensive one — throttle on the device, and write the ping stream somewhere built for it.
iOS: two strings, a capability and a flag
iOS needs the usage strings for both authorisation levels, the location background mode in Info.plist, and the matching Background Modes capability enabled on the target. All three, or updates stop at the lock screen.
<key>NSLocationWhenInUseUsageDescription</key>
<string>Shows your position to the dispatcher while you are on shift.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Keeps sharing your position while the app is in your pocket, so
customers see the delivery move and dispatch can route the next order.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>Apple attaches a condition to that background mode that is worth reading as written: an app declaring it must have features that genuinely require persistent location, and an app without such features is told to remove it and use significant-change or region monitoring instead. A rider on a shift is squarely inside that. A customer watching their order is not — that screen is in the foreground with a map open, and does not need the mode at all.
The other iOS-shaped surprise is that you cannot ask for Always up front. The system grants While Using first and offers the upgrade later, on its own schedule, or through Settings. There is no foreground-service equivalent to fall back on, so the honest design is a screen that explains what is missing and routes the rider to the setting — and a dispatcher view that can tell the difference between a rider who is offline and a rider whose permission is downgraded.
Play reads a form and watches a video
Background location is a declared permission at Google Play, and the declaration is reviewed by a person. The form asks you to name one feature — one, not a list — that requires it, to say why the same experience cannot be delivered with foreground access, and to attach a video of thirty seconds or less. The video has to show the app opening, the route to your disclosure screen with the text legible, what happens when the user consents, what happens when they decline, and the feature working.
The disclosure itself has rules of its own. It has to appear in normal use of the app rather than behind a menu, it has to name the data and what happens to it, and the wording has to carry both the word location and the fact that collection continues in the background. Google's own shape for the sentence is close to: collects location data to enable a named feature even when the app is closed or not in use. A line in your privacy policy is not a disclosure, and a disclosure bundled in with unrelated consents is not one either.
And the loophole is not a loophole. A foreground service is not a route around the policy — where the service is doing what background location access does, the same declaration requirements apply to it.
For anyone shipping a bought template, the practical consequence is a planning one. The disclosure screen and a thirty-second screen recording are release artefacts with a review queue attached, not a code change you make on the last afternoon.
What changes on 27 January 2027
Play announced a minimum-scope policy for location in April 2026, and it is worth knowing now because it lands mid-life for anything you ship this year. For apps targeting Android 17 (API level 37) and above, the Android location button becomes the required way to obtain precise location for transactional, one-time use. Declarations open in Play Console in November 2026 and enforcement begins on 27 January 2027.
Continuous precise tracking for a core feature — navigation, fitness, a rider on a shift — remains allowed. What changes is that it becomes a declaration you defend rather than a default you inherit, and there is no blanket carve-out for delivery apps. Access has to be essential to the main purpose of the app as your store listing describes it, which is an argument the rider app wins easily and the customer app does not.
That is the half of the codebase this really touches. "Use my current location" at checkout is precisely the transactional case the button exists for, and a customer app that asks for fine location to prefill an address field is asking for something it will shortly have to justify. It should be asking for the button instead.
Three things this post deliberately leaves alone: detecting arrival at the pickup or drop-off, which is geofencing and has its own battery story; choosing which rider gets an order; and where the location feed is stored. Each is a separate decision from keeping the feed alive.
Tie tracking to the shift, not to the app
Start the service from a foreground tap, with a notification that says plainly what it is doing and who can see it. Stop it when the rider goes off shift, and stop it again defensively when the last assigned order is delivered. A service that survives the night is a battery complaint, a one-star review, and an awkward answer at the next policy review — and none of those show up in a crash report either.
Then run the only test that matters before any of the others. Go on shift, lock the phone, put it in a pocket, and walk for twenty minutes. Everything above is a way of passing that test; nothing above is worth anything until you have.
Feastly — $39
Customer, rider, vendor and a Next.js admin on one backend, with live order tracking and the rider location feed already wired between them.