Geofencing
BGeo geofences are app-facing circular regions you define —
Transistor-shaped CRUD, distinct from the
engine’s own internal wake geofence used for
killed-app relaunch. You register them with an identifier, a centre, and a
radius; the SDK tells you (via onGeofence)
when the device enters, exits, or dwells inside one.
Geofences can be added, removed, and listed at any time — tracking does not
need to be running. OS-level monitoring, however, is only active while
tracking is started (start()):
stop() unregisters every app-facing region from the OS (the persisted set
is untouched), and start() re-registers them. Forcing a motion-state change
with changePace() has no
effect on geofence monitoring either way — only tracking start/stop does.
Adding and removing geofences
Full method signatures, parameters, and return types are documented on the
Methods reference; this
section covers the fields and the validation rules behind INVALID_GEOFENCE.
A geofence rejects with INVALID_GEOFENCE unless all of the following hold:
identifieris a non-empty string.radiusis a finite number greater than0.latitudeis finite and within-90..90.longitudeis finite and within-180..180.- at least one of
notifyOnEntry,notifyOnExit,notifyOnDwellistrue(a geofence that notifies on nothing is rejected rather than silently registered as dead weight).
BackgroundGeolocation.addGeofence( Geofence( identifier = "home", radius = 150.0, latitude = 52.2297, longitude = 21.0122, notifyOnEntry = true, notifyOnExit = true, notifyOnDwell = true, loiteringDelay = 300_000.0, // 5 minutes, required for a DWELL transition extras = JSONObject().put("kind", "home").put("label", "Home"), ),)extras is an arbitrary Map<String, dynamic> you attach to the geofence;
it is never inspected by the SDK and is echoed back verbatim on every
GeofenceEvent so you
can identify which geofence fired without a lookup.
Adding a geofence whose identifier already exists overwrites it in
place — new radius, coordinates, notify flags, and extras replace the old
ones, and if it’s currently OS-registered it’s re-registered with the fresh
values. There is no separate “update” call.
addGeofences() is the bulk form — same validation, same overwrite
semantics, applied to a whole List<Geofence> in one native round-trip:
BackgroundGeolocation.addGeofences( listOf( Geofence(identifier = "home", radius = 150.0, latitude = 52.2297, longitude = 21.0122, notifyOnEntry = true, notifyOnExit = true), Geofence(identifier = "office", radius = 100.0, latitude = 52.2319, longitude = 21.0067, notifyOnEntry = true, notifyOnExit = true), Geofence(identifier = "school", radius = 120.0, latitude = 52.2189, longitude = 21.0289, notifyOnEntry = true, notifyOnDwell = true, loiteringDelay = 600_000.0), ),)Removing is either targeted or total — there is no “remove several by id” call:
BackgroundGeolocation.removeGeofence("office") // just this one
BackgroundGeolocation.removeGeofences() // ALL geofences, the whole persisted setgetGeofences() and geofenceExists() read the full persisted set — not
just the proximity-sliced subset currently registered with the OS (see
Proximity slicing below):
BackgroundGeolocation.onGeofence { event -> Log.d("BGeo", "${event.action} ${event.identifier} extras=${event.extras}")}
BackgroundGeolocation.onGeofencesChange { event -> // The OS is now watching event.on and has released event.off — every // fence you added still exists either way.}Radius guidance
There’s no platform-enforced minimum radius in this SDK — any value greater
than 0 passes validation. In practice, a larger radius makes ENTER/EXIT
more reliable: GPS accuracy jitter near a small boundary can produce spurious
crossings, especially indoors or downtown. A tighter radius gives more
precise triggering at the cost of occasional missed or bouncing transitions.
Pick the radius per use case rather than a single fixed number.
Geofence events: ENTER, EXIT, DWELL
Transitions are delivered through onGeofence
as a GeofenceEvent —
see that reference for the full payload shape. This section covers only the
semantics of the three actions.
- ENTER fires when the device crosses into the region, if
notifyOnEntryistrue. - EXIT fires when the device crosses back out, if
notifyOnExitistrue. - DWELL fires after the device has remained inside the region for at
least
loiteringDelaymilliseconds, ifnotifyOnDwellistrue. AnotifyOnDwellgeofence without a positiveloiteringDelayfalls back to a 5-minute (300000) default rather than disabling DWELL.
DWELL is implemented differently per platform, though the observed event is identical:
- Android uses the OS’s own native dwell detection
(
Geofence.GEOFENCE_TRANSITION_DWELL+setLoiteringDelay) — Play Services tracks the loiter internally and delivers the transition.
Proximity slicing
Android imposes a hard cap on how many geofences an app can have registered with the OS at once — 99 (each platform’s real budget is 20 / 100, minus one region the SDK’s own internal wake geofence occupies). Persisted geofences aren’t limited to that count — you can store as many as you like — but only a subset can be live with the OS simultaneously. The SDK bridges this gap with proximity slicing:
- Of all persisted geofences, only those within
geofenceProximityRadiusmetres of the device’s last accepted fix are candidates for OS registration (default1000m). - Candidates are sorted by distance and the nearest ones are registered, up
to the platform budget — or a smaller number if you set
maxMonitoredGeofencesto deliberately stay under budget (<= 0, the default, uses the platform budget as-is). - On every accepted location fix, the slice is re-evaluated: geofences that fell out of range are unregistered and newly-in-range ones are registered, keeping the OS-registered set centred on where the device actually is.
- Before any fix has been received (cold start), the SDK falls back to registering the first geofences in persisted order, re-slicing as soon as the first fix arrives.
flowchart LR
store[(All persisted geofences)] --> filter[Proximity filter\nwithin geofenceProximityRadius\nof last fix]
filter --> sort[Sort by distance]
sort --> cap[Cap at min of\nmaxMonitoredGeofences and\nthe platform budget of 99]
cap --> registered[OS-registered subset]
registered -.-> change[onGeofencesChange: on / off delta]
fix[New accepted fix] -.-> filter
onGeofencesChange
tells you exactly which geofences are in the active, OS-registered set at
any moment: it’s a delta, not the full set — on lists geofences newly
registered, off lists geofences just unregistered, fired whenever the
slice rotates. Both are List<Geofence>. See
GeofencesChangeEvent
for the payload shape.
Synthetic ENTER on registration
geofenceInitialTriggerEntry
(default true) requests a synthetic ENTER for any geofence that turns out
to already contain the device at the moment it’s registered with the OS
(INITIAL_TRIGGER_ENTER). Without it,
a geofence you’re already standing inside when it’s added — or when
proximity slicing brings it into the OS-registered set — would otherwise
never fire ENTER until you leave and re-enter. Set it false if you only
want events for boundary crossings that happen after registration.
Persistence
Geofences persist in the SDK’s own SQLite database (bgeo.db, in the
geofences table) on both platforms, alongside the location queue — see the
data pipeline concept.
They survive app restarts and device reboots: the full set is reloaded and
re-registered with the OS the next time tracking starts, with no need to
call addGeofence() again after every launch.
Geofence ENTER/EXIT/DWELL transitions ride the same upload queue as
location records rather than a separate channel — a transition is queued and
uploaded exactly like a
location fix, including delivery while the app was killed at the moment it
happened. Your server receives it as a location-shaped record with an event
field identifying it as a geofence transition, not a plain location.
Try it in the example app
The example app is a single-screen demo
without a map view, so there’s no long-press-to-create-geofence flow to try
here the way there is on the React Native example — exercise
addGeofence()/onGeofence directly from your own code using the snippets
above, and watch the transitions arrive via
getLog() or the
web console’s log stream once a device
is linked.
Long-press the map to open the creation form.
The saved geofence on the map, alongside the tracked route.