Skip to content

Background execution

Background tracking is the most divergent area between platforms. The state machine, 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
SharedPreferences (in.wayq.beekon)persists tracking intent so a host-driven cold launch can resume

The foreground notification is required by the platform, not a Beekon design choice. The notification channel id and notification id are SDK-internal constants; the title, body and small icon are configurable via BeekonConfig.Notification.

Android 15+ blocks BOOT_COMPLETED from launching foreground services, and Android has no equivalent to iOS’s Significant Location Change wake. If you want background tracking to survive process death, call Beekon.start() from your Application.onCreate — the SDK rehydrates the last BeekonConfig and re-arms the gate. There’s no boot receiver and no JobScheduler periodic on the SDK’s side.

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). Surface those user-facing instructions in your app’s onboarding for affected devices. When the service is killed entirely, Beekon transitions state to Stopped(System); your app calls start() again when appropriate.

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 fixes during these periods rather than dropping mid-trip.

iOS background execution is fundamentally about whether your app is alive at all. Beekon’s iOS 17+ stack is a single code path:

CLLocationUpdate.liveUpdates(.default)
+ 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. The configuration is fixed at CLLocationUpdate.LiveConfiguration.default — power-profile tuning isn’t exposed publicly. Rate is governed by Beekon’s interval + distance gate.

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, the host app drives resume:

  1. Call await Beekon.shared.start() early in your App init.
  2. Beekon reads the persisted tracking intent from a UserDefaults suite (in.wayq.beekon); if no intent is saved it falls back to BeekonConfig.default.
  3. The fresh process has a new state stream — re-subscribe; the latest state replays.

Force-quit by the user permanently disables SLC until the next foreground open — that’s an Apple platform behaviour, not something Beekon controls.

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 bridges sit on top of the native libraries — Beekon’s background execution on Android and iOS works the same whether you’re on a native or bridged app. The Flutter and JS engines can be torn down in background; that’s why the persistence write path lives in the native library, not in Dart or JavaScript.

When the bridge engine is alive:

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

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

  • The native library keeps tracking; fixes accumulate in the DB.
  • Streams emit nothing to Dart / JS — there’s no consumer to emit to.
  • Relaunching the app gets you back the live state and the full history.

This means you should never rely on a Dart-side or JS-side stream listener to react to a background fix. 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.