Skip to content

Background execution

Background tracking is the most divergent area between platforms. The state machine, presets, gate, and schema are uniform — but the OS mechanics that keep your app alive differ fundamentally. Beekon picks the right mechanism per platform; you just need to understand what it’s doing so you can debug when it doesn’t.

Beekon runs a foreground service typed FOREGROUND_SERVICE_LOCATION (Android 14+) backed by FusedLocationProviderClient. The service holds a partial wake lock for the duration of tracking.

MechanismRole
FusedLocationProviderClientthe actual location source — fuses GPS, Wi-Fi, cell, sensor signals
Foreground service + notificationsatisfies Android 8+ background-location rules; visible to user
FOREGROUND_SERVICE_LOCATION typerequired for Android 14+ to start a location FGS
Boot receiverauto-restarts tracking after device reboot if it was previously enabled
WorkManagerrestart-if-killed, deferred work hook

The foreground notification is required by the platform, not a Beekon design choice. Pick a reasonable channel name and body; Beekon creates the notification channel on Android 8+ on your behalf.

Even with everything wired correctly, aggressive OEM battery managers can kill foreground services on devices from Xiaomi, Huawei, Samsung, OPPO, Vivo, and others. The dontkillmyapp.com matrix maps OEM-specific workarounds (auto-start whitelisting, battery-optimisation exemption, etc.). Surface those user-facing instructions in your app’s onboarding for affected devices.

Beekon’s state holder survives the service’s death and respawn — your state and positions Flow identities don’t change across service restarts. If the OS kills the service entirely, WorkManager schedules a restart and the boot receiver covers reboot.

Android’s Doze mode (device idle) and App Standby (per-app idle) reduce wake-up frequency for backgrounded apps. The foreground service exempts you from most of this, but not all of it — expect coarser update cadence during deep doze. Beekon’s gate means you simply emit fewer positions during these periods rather than dropping mid-trip.

iOS background execution is fundamentally about whether your app is alive at all. Beekon picks the modern API on iOS 17+ and falls back to the legacy delegate on iOS 15–16 — your app sees one shape via Beekon.shared.

CLLocationUpdate.liveUpdates(LiveUpdateConfiguration)
+ CLBackgroundActivitySession // keeps background delivery alive
+ CLServiceSession(authorization: .always) // iOS 18+ only, declared-authorization

CLLocationUpdate.liveUpdates is the async sequence Apple introduced to replace the delegate. CLBackgroundActivitySession is what makes it survive backgrounding. Beekon holds these in StateHolder.

The iOS 17+ path’s preset configuration maps to the framework’s preset:

Beekon presetCLLocationUpdate configuration
Saver.default
Balanced.default
Precision.automotiveNavigation
CLLocationManager
.startUpdatingLocation()
.allowsBackgroundLocationUpdates = true
.showsBackgroundLocationIndicator = true

Wrapped in an AsyncStream so StateHolder consumes one shape regardless of OS.

Significant Location Change (every version)

Section titled “Significant Location Change (every version)”

Beekon runs a Significant Location Change monitor alongside liveUpdates. SLC is the only mechanism that wakes a terminated iOS app from coarse movement; liveUpdates does not.

When SLC wakes your app into a fresh process:

  1. Beekon reads the persisted tracking intent from a UserDefaults suite (com.wayq.beekon).
  2. If the app was tracking before termination, Beekon auto-resumes tracking on relaunch.
  3. The fresh process has a new state stream — re-subscribe; the latest state replays.

This is why you don’t need a restoreTracking() call — Beekon does it.

You need Always authorization for background tracking. iOS forces a two-step prompt: WhenInUse first, then Always after the user has used the relevant feature once. Asking for Always straight away is silently denied.

Beekon does not drive these prompts — the host app does, because timing and copy depend on your UX. See iOS setup.

The plugin sits on top of the native libraries — Beekon’s background execution on Android and iOS works the same whether you’re on a native or Flutter app. The Flutter engine itself can be torn down in background; that’s why the persistence write path lives in the native library, not in Dart.

When the Flutter engine is alive:

  • state and positions streams flow normally.
  • history(from, to) queries the native DB.

When the Flutter engine is dead (background, terminated):

  • Native library keeps tracking; positions accumulate in the DB.
  • Streams emit nothing to Dart — there’s no Dart to emit to.
  • Re-launching the app gets you back the live state and the full history.

This means you should never rely on a Dart-side stream listener to react to a background position. Use the native DB and read it on resume.

For end-to-end background validation, the sample apps in the repo are the canonical test rigs:

  • beekon-android/sample — Compose app with start/stop, live map, history view
  • beekon-ios/Sample — SwiftUI app, equivalent shape
  • beekon_flutter/example — Flutter app, exercises both platforms

A real-device 30–60 minute walk with the app backgrounded is the only way to truly verify background reliability — emulators and simulators lie about doze/SLC.