Events
Import the facade — every on* method below is a static method on it:
import 'package:bgeo_background_geolocation/bgeo_background_geolocation.dart' as bg;Subscription model
Every on* method registers a callback and returns a plain Dart
StreamSubscription<T>
— this is the one API-shape difference from the RN/Transistor SDKs, which
return a Subscription object with a .remove() method. Call .cancel() on
the returned subscription to detach that one listener:
final sub = bg.BackgroundGeolocation.onLocation((location) { print('location ${location.coords.latitude}, ${location.coords.longitude}');});
// later, e.g. in State.dispose()sub.cancel();To tear down every listener at once (including geofence listeners and
the watchPosition() wiring), call
removeListeners()
instead of cancelling them one at a time — it cancels every subscription
handed out by the onX methods on this page, plus any active
watchPosition().
Events are only delivered while the Dart isolate is running. A killed app, or one that hasn’t been launched since boot, cannot receive any of the events below — the native side keeps tracking, filtering, persisting, and uploading without you. See the boot & killed-app behavior guide for what happens instead, and the “Headless” line under each event for whether it also reaches the Android headless task while the Dart isolate isn’t running.
onLocationError is a real, separate on* method here (unlike RN, where
locationerror only surfaces through watchPosition()’s onError
callback) — see onLocationError below.
Location & motion
onLocation
static StreamSubscription<Location> onLocation(void Function(Location) cb)Fires for every accepted location fix — both the ambient continuous stream
(whenever tracking is enabled) and, if you’ve separately called
watchPosition(), those
same fixes ride this same underlying native location event.
watchPosition() distinguishes its own fixes with an extras['watch']
marker and filters on it internally, but onLocation() does not — if
watchPosition() is active, your onLocation() listeners also receive its
fixes, unfiltered.
Payload: Location.
Headless: No. High-frequency location fixes are deliberately excluded from headless dispatch — forwarding every fix would spin up a background isolate (and a wakelock) per fix while tracking, draining the battery and risking ANRs/foreground-service-start restrictions. Fixes taken while the Dart isolate isn’t running are still durably queued and uploaded by the native HTTP store; they’re just not delivered as a headless event.
final sub = bg.BackgroundGeolocation.onLocation( (location) => print('fix ${location.coords} ${location.isMoving}'),);
// ...sub.cancel();onLocationError
static StreamSubscription<int> onLocationError(void Function(int) cb)Fires with a numeric error code when a location request fails. Unlike
onLocation’s failure parameter on the RN SDK (which is accepted but
never invoked), this is a real, independently-wireable event stream.
Payload: int — a native error code.
Headless: No.
final sub = bg.BackgroundGeolocation.onLocationError( (errorCode) => print('location error $errorCode'),);
// ...sub.cancel();onMotionChange
static StreamSubscription<MotionChangeEvent> onMotionChange(void Function(MotionChangeEvent) cb)Fires whenever the automatic motion-state machine flips between moving and
stationary, and also when you force a transition with
changePace().
Payload: MotionChangeEvent
— { isMoving, location }, where location is nullable and null on the
first motionchange of a tracking session, before any fix exists yet.
Always guard before dereferencing it.
Headless: Yes — motionchange is in the Android headless event set.
final sub = bg.BackgroundGeolocation.onMotionChange((event) { final location = event.location; if (location != null) { print('${event.isMoving ? "started moving" : "stopped"} ${location.coords}'); } else { print('${event.isMoving ? "started moving" : "stopped"} (no fix yet)'); }});
// ...sub.cancel();onHeartbeat
static StreamSubscription<HeartbeatEvent> onHeartbeat(void Function(HeartbeatEvent) cb)Fires every heartbeatInterval
seconds (default 60) unconditionally while tracking is enabled — not gated
behind a separate opt-in. Each firing also runs a safety check against the
stop-timeout countdown.
Payload: HeartbeatEvent
— location is nullable, null if no fix has been taken yet in the current
session.
Headless: Yes — heartbeat is in the Android headless event set.
final sub = bg.BackgroundGeolocation.onHeartbeat((event) { print('heartbeat ${event.location?.coords}');});
// ...sub.cancel();Permissions & power
onProviderChange
static StreamSubscription<ProviderChangeEvent> onProviderChange(void Function(ProviderChangeEvent) cb)Fires when the OS location-provider or authorization state changes — a
permission grant/revoke, GPS or network provider toggled, or Location
Services switched off entirely. This is the same shape resolved by the
one-shot getProviderState().
Payload: ProviderChangeEvent.
Headless: Yes — providerchange is in the Android headless event set.
final sub = bg.BackgroundGeolocation.onProviderChange((event) { print('provider ${event.status} ${event.enabled} ${event.gps}');});
// ...sub.cancel();onPowerSaveChange
static StreamSubscription<bool> onPowerSaveChange(void Function(bool) cb)Fires when the OS power-saving state changes (Android battery saver, iOS Low Power Mode).
Payload quirk: the callback receives a bare bool, not an event
object. Natively the engine emits {isPowerSaveMode: bool}; the Dart facade
unwraps it before invoking your listener (Transistor callback parity). This
unwrap only happens on the live-listener path — see the Headless note below.
Headless: Yes — powersavechange is in the Android headless event set,
but the bare-boolean unwrap above is listener-only. The headless task
receives the unwrapped native shape instead: a
HeadlessEvent with
name: 'powersavechange' and params: {'isPowerSaveMode': bool}.
final sub = bg.BackgroundGeolocation.onPowerSaveChange( (isPowerSaveMode) => print('power save mode: $isPowerSaveMode'),);
// ...sub.cancel();HTTP & connectivity
onHttp
static StreamSubscription<HttpEvent> onHttp(void Function(HttpEvent) cb)Fires once per completed location-sync HTTP request — log uploads (see
the logging guide) do not emit this event. A
401/403 that triggers a token refresh-and-retry emits two events: one
for the failed original request and one for the retried request.
Payload: HttpEvent.
Headless: Yes — http is in the Android headless event set.
final sub = bg.BackgroundGeolocation.onHttp((event) { if (!event.success) { print('upload failed ${event.status} ${event.responseText}'); }});
// ...sub.cancel();onConnectivityChange
static StreamSubscription<ConnectivityChangeEvent> onConnectivityChange(void Function(ConnectivityChangeEvent) cb)Fires on validated-internet connectivity transitions — Android
NET_CAPABILITY_VALIDATED / iOS NWPath.satisfied — not raw radio/Wi-Fi
association. A captive-portal Wi-Fi network (connected to the AP, no real
internet) reports connected: false, not true.
Synthetic initial delivery (subtle): immediately after subscribing, this
method also calls getState()
internally to fetch the current connectivity and deliver it to your callback
as a synthetic first event — so you don’t have to call anything else to
learn the state at subscribe time (Transistor/RN parity). This is a genuine
network round-trip to the platform channel, not synchronous, so two things
can beat it:
- A real
connectivitychangeevent arrives first. The listener sets an internal flag the moment any real event is delivered; when thegetState()round-trip resolves afterward, it checks that flag and silently skips the synthetic delivery if a real event already fired. - The subscription is cancelled before the round-trip resolves. Calling
.cancel()on the returned subscription marks it removed; whengetState()resolves afterward, the synthetic delivery is silently skipped rather than invoking a callback whose owner has already torn down.
In both cases the skip is silent — no error, the callback is just never invoked with the synthetic value. This replay is listener-side only and does not apply to the headless path.
Payload: ConnectivityChangeEvent.
Headless: Yes — connectivitychange is in the Android headless event
set. (The subscribe-time replay above never applies headlessly — headless
dispatch only ever carries real transitions.)
final sub = bg.BackgroundGeolocation.onConnectivityChange((event) { print('connected: ${event.connected}');});
// ...sub.cancel();onAuthorization
static StreamSubscription<AuthorizationEvent> onAuthorization(void Function(AuthorizationEvent) cb)Fires with the outcome of a native, killed-app-safe token refresh —
configured via Config.authorization.
The uploader triggers this refresh on a 401/403 with no Dart isolate
required, so this event is how your app learns a background refresh
happened (and should prefer
getAuthState() over its
own persisted tokens afterward).
Payload: AuthorizationEvent
— { success, accessToken?, refreshToken?, status? }.
Headless: No. authorization is not present in the Android engine’s
headless event set.
final sub = bg.BackgroundGeolocation.onAuthorization((event) { if (event.success) { print('token refreshed ${event.accessToken}'); } else { print('token refresh failed ${event.status}'); }});
// ...sub.cancel();Geofencing
onGeofence
static StreamSubscription<GeofenceEvent> onGeofence(void Function(GeofenceEvent) cb)Fires on a transition for any geofence currently registered with the OS (the
proximity-sliced subset — see the
Geofencing guide). DWELL requires
notifyOnDwell + loiteringDelay on the geofence and is implemented
natively on Android; on iOS it’s a timer armed after ENTER and cancelled on
EXIT before it fires. Geofence transitions ride the same durable upload
queue as location records, so they’re delivered to your server even if the
app was killed when the transition happened.
Payload: GeofenceEvent
— { identifier, action, location, extras? }.
Headless: Yes — geofence is in the Android headless event set.
final sub = bg.BackgroundGeolocation.onGeofence((event) { print('geofence ${event.identifier}: ${event.action}');});
// ...sub.cancel();onGeofencesChange
static StreamSubscription<GeofencesChangeEvent> onGeofencesChange(void Function(GeofencesChangeEvent) cb)Fires when the proximity-sliced subset of geofences actually registered with
the OS changes — e.g. as you move and different geofences fall in or out of
geofenceProximityRadius.
This is a delta, not the full set: on is the geofences newly
registered, off is the geofences just unregistered. See the
Geofencing guide for the proximity-slicing
algorithm and platform registration budgets.
Payload: GeofencesChangeEvent
— { on: List<Geofence>, off: List<Geofence> }.
Headless: No. geofenceschange is not present in the Android engine’s
headless event set — only the transition event (onGeofence) reaches the
headless task; a proximity-slice recompute does not.
final sub = bg.BackgroundGeolocation.onGeofencesChange((event) { print('+${event.on.length} geofences, -${event.off.length} geofences');});
// ...sub.cancel();