Skip to content

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:

  • identifier is a non-empty string.
  • radius is a finite number greater than 0.
  • latitude is finite and within -90..90.
  • longitude is finite and within -180..180.
  • at least one of notifyOnEntry, notifyOnExit, notifyOnDwell is true (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 set

getGeofences() 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 notifyOnEntry is true.
  • EXIT fires when the device crosses back out, if notifyOnExit is true.
  • DWELL fires after the device has remained inside the region for at least loiteringDelay milliseconds, if notifyOnDwell is true. A notifyOnDwell geofence without a positive loiteringDelay falls 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 geofenceProximityRadius metres of the device’s last accepted fix are candidates for OS registration (default 1000 m).
  • Candidates are sorted by distance and the nearest ones are registered, up to the platform budget — or a smaller number if you set maxMonitoredGeofences to 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.

The Android example app's New geofence form, opened by long-pressing the map: the pressed coordinates, an identifier field, a radius field defaulting to 200 m, and the notify-on ENTER, EXIT and DWELL switches above Save.

Long-press the map to open the creation form.

The Android example app's map after saving: the geofence drawn as an orange circle with its marker, alongside the tracked route.

The saved geofence on the map, alongside the tracked route.