Events
Each event has two equivalent shapes: a Flow property for structured
concurrency, and an onX(handler) callback returning a Subscription. Same
events, same payloads — the difference is in how delivery behaves under load.
Flow or callback?
A callback runs inline on whatever engine thread emitted the event. It is never dropped, and a slow handler stalls that engine thread until it returns. Backpressure, in other words: the engine waits for you.
A Flow buffers without limit. A slow collector never blocks the emitter
and never silently loses an event — but an indefinitely stalled collector lets
that buffer grow without bound.
Prefer a Flow when you want the emitting thread left alone. Prefer onX when
you would rather have backpressure than unbounded memory growth. Do not do
blocking I/O in either without moving it off the emitting thread first.
Every access to a Flow property mints its own subscription, so two collectors
of locations each receive every fix.
Subscribe before ready()
Events that arrive while the engine is coming up are buffered and replayed to
the first subscriber, so subscribing before ready() loses nothing. Subscribing
after it can miss the launch-time providerchange a cold start emits.
onLocation
BackgroundGeolocation.locations.collect { location -> }BackgroundGeolocation.onLocation { location -> }Every accepted fix, after filtering. Payload:
Location.
Fixes rejected by the accuracy or teleport filter never appear here — if the
stream is quiet, compare lastRawFixAge and lastAcceptedFixAge in
getState() before assuming the OS stopped delivering.
onMotionChange
BackgroundGeolocation.motionChanges.collect { event -> }BackgroundGeolocation.onMotionChange { event -> }The motion state machine flipped between moving and stationary. Payload:
MotionChangeEvent.
event.location is null on the first motion change of a session — the
engine has not accepted a fix yet at that point.
onProviderChange
BackgroundGeolocation.providerChanges.collect { state -> }BackgroundGeolocation.onProviderChange { state -> }Location authorization or provider availability changed: the user granted or
revoked permission, turned location services off, or switched to approximate
location. Payload:
ProviderState.
This is how you learn tracking has gone quiet for a reason you can explain to the user, rather than discovering it from an empty map.
onHeartbeat
BackgroundGeolocation.heartbeats.collect { event -> }BackgroundGeolocation.onHeartbeat { event -> }Fires every heartbeatInterval seconds while stationary. It is a
keep-alive tick, not a location: use it to poll getCurrentPosition() if you
need a fix while parked, and expect Doze to stretch the interval.
onHttp
BackgroundGeolocation.httpEvents.collect { event -> }BackgroundGeolocation.onHttp { event -> }The result of every upload request the engine made. Payload:
HttpEvent.
status = 0 means the request never reached a server (DNS, TLS, no route) —
distinct from a 5xx, and the engine retries it rather than dropping the record.
onConnectivityChange
BackgroundGeolocation.connectivityChanges.collect { event -> }BackgroundGeolocation.onConnectivityChange { event -> }Validated internet connectivity changed — a captive portal counts as disconnected, unlike a bare “interface up” signal.
onPowerSaveChange
BackgroundGeolocation.powerSaveChanges.collect { isPowerSaveMode -> }BackgroundGeolocation.onPowerSaveChange { isPowerSaveMode -> }The device entered or left battery-saver mode. The engine unwraps the native
{isPowerSaveMode} payload to a bare Boolean.
Battery saver throttles background location hard; this event is often the explanation for a track that suddenly goes sparse.
onAuthorization
BackgroundGeolocation.authorizationEvents.collect { json -> }BackgroundGeolocation.onAuthorization { json -> }The engine’s HTTP layer refreshed (or failed to refresh) your JWT pair.
Payload is a raw JSONObject: {success, accessToken, refreshToken}.
It carries live credentials. Persist them if your app keeps its own copy —
see getAuthState() — but do
not log the object. The BGeo example console logs only presence booleans for
exactly this reason.
onLocationError
BackgroundGeolocation.locationErrors.collect { error -> }BackgroundGeolocation.onLocationError { error -> }A watchPosition() tick failed, or watchPosition() was called on an
unlicensed build. Payload:
BGeoException.
Both of those sites emit this event instead of throwing, so a failing watch
fails in complete silence without a subscriber here. If you use
watchPosition(), subscribe to this.
onGeofence
BackgroundGeolocation.onGeofence { event -> }A geofence transition: ENTER, EXIT or DWELL. Payload:
GeofenceEvent.
onGeofencesChange
BackgroundGeolocation.onGeofencesChange { event -> }The set of geofences the OS is actively monitoring changed as you moved —
Android caps how many regions one app can watch, so the engine keeps the
nearest ones armed. Payload:
GeofencesChangeEvent.
Both geofence events are extension functions declared alongside the geofence
CRUD in Geofences.kt, so they need the same import as addGeofence().
Unsubscribing
val subscription = BackgroundGeolocation.onLocation { }subscription.remove()removeListeners() detaches every callback subscription at once. It does
not stop a Flow collector: each Flow mints its own channel, and a
collector already running stays suspended forever after the underlying
subscription is cleared. Cancel the collecting coroutine or its scope instead —
lifecycleScope and repeatOnLifecycle do this for you.