Flutter Background Location Tracking When App Is Killed: Using Android Foreground Service

Background location tracking in Flutter is one of those problems that looks straightforward until you actually try to build it for production. The pub.dev ecosystem has packages that promise to handle it, but the well-maintained ones come with licensing fees — around $375 per app per year at the time I was working on this. For a commercial project, that's a reasonable cost. For a self-funded one, it's not.
So I built it using Android's native foreground service directly. This is a writeup of how I got there and why the approach works where most alternatives fail.
Why Most Packages Fail When the App Is Killed
The root cause is threading. Most Flutter location packages run their location logic on the Flutter engine's main UI thread. When Android kills the app — whether because the user swiped it away, the OS reclaimed memory, or the device restarted — that thread stops. The location tracking stops with it.
A foreground service is different. It runs on its own thread, independent of the app's lifecycle. Android treats foreground services with higher priority than background processes, which means the OS is far less likely to terminate them under memory pressure. They also persist across app restarts because they're registered with Android as running services, not just background tasks attached to an activity.
The other key insight: any work you do from the service thread — API calls, Firebase writes, database updates — needs to happen within that thread. If you try to hand off data back to Flutter and then push it to Firebase from the Dart side, you reintroduce the dependency on the Flutter engine being alive. The service has to own the full chain: collect location, push to backend, done.
The Approach
The architecture I landed on has three parts:
An Android foreground service that runs independently, requests location updates from FusedLocationProviderClient, and pushes coordinates to Firebase Realtime Database directly from the native layer. It displays a persistent notification showing the current coordinates — both to meet Android's foreground service requirements (a visible notification is mandatory) and as a useful debugging tool during development.
A Flutter-to-native bridge using a MethodChannel to start and stop the service. From the Flutter side, you call startService or stopService. The native side registers or kills the foreground service accordingly.
The Flutter UI layer which shows the current state (tracking active/inactive) and lets the user control it. The UI doesn't receive location updates directly — it reads from Firebase if it needs to display current position, keeping the UI and the tracking service decoupled.
Why a Foreground Service Specifically
Android has several service types. A regular background service gets killed aggressively on newer Android versions — sometimes within minutes. A JobScheduler or WorkManager task runs on a schedule but isn't appropriate for continuous real-time tracking. A foreground service with a visible notification is the only option Android explicitly supports for long-running, user-aware background work.
The notification requirement isn't optional. Android will throw an exception if you try to run a foreground service without starting it with a notification. The notification tells the user "this app is actively doing something in the background" — which is the right contract to have with the user for location tracking anyway.
Integrating with Flutter
The MethodChannel wiring is the same pattern as any other Flutter-to-native communication. On the Dart side:
const _channel = MethodChannel('com.yourapp/location_service');
Future<void> startTracking() async {
await _channel.invokeMethod('startLocationService');
}
Future<void> stopTracking() async {
await _channel.invokeMethod('stopLocationService');
}
On the Android (Kotlin) side, handle these calls in MainActivity and start or stop the foreground service in response. The service itself is a standard Android Service subclass that calls startForeground() in onStartCommand.
Firebase Writes From the Service Thread
This is where most implementations fall apart. The temptation is to collect location in the native service, pass it back to Flutter via a method channel, and then write to Firebase from Dart. Don't do this.
When the app is killed, the Flutter engine is gone. The method channel is gone. Any pending location updates sitting in a queue waiting to be passed to Dart disappear.
Instead, initialize the Firebase Android SDK directly in the service class and write to Realtime Database or Firestore from there. The Firebase Android SDK works perfectly from a foreground service. Your location data reaches Firebase whether or not the Flutter UI is running.
Permissions
Background location tracking requires explicit permissions. In AndroidManifest.xml:
<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" />
ACCESS_BACKGROUND_LOCATION was added in Android 10 and must be requested separately from fine location — the user has to explicitly grant "Allow all the time" rather than just "Allow while using the app." Google Play also requires justification for this permission during app review.
On Android 14 and above, FOREGROUND_SERVICE_LOCATION is additionally required.
What This Approach Doesn't Cover
iOS is a different story entirely. iOS doesn't have foreground services in the Android sense. Continuous background location on iOS requires a different entitlement (Location updates background mode) and has stricter restrictions on what the app can do while backgrounded. This approach is Android-specific.
For cross-platform background location with iOS support, you'll need either a paid package or a separate native iOS implementation.
The Result
The foreground service approach works reliably across the Android versions I tested — from Android 9 through Android 14. The app can be killed by the user, the phone can restart, and as long as the service was running when those events happened, it continues running afterward (Android restarts foreground services automatically after a device reboot if START_STICKY is returned from onStartCommand).
The notification is a small UX cost, but for apps doing delivery tracking, attendance, or any use case where the user knows the app is tracking them, it's the right expectation to set. It's also honest — the user can see exactly when tracking is active.
If you're building something that genuinely needs persistent background location on Android and the package licensing fees aren't feasible, the native foreground service approach is worth the setup cost. It's more code, but it's reliable in a way that Flutter-layer solutions typically aren't.




