Skip to content

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.