Skip to content

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: HeartbeatEventlocation 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 connectivitychange event arrives first. The listener sets an internal flag the moment any real event is delivered; when the getState() 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; when getState() 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();