Geofencing API
Tracelet provides a robust, offline-first geofencing engine. Unlike standard OS geofencing which is limited to 20-100 geofences and requires the OS to wake your app, Tracelet evaluates geofences internally within the Rust engine. This enables unlimited geofences with zero additional battery drain.
1. Circular vs Polygon Geofences
Tracelet supports both standard circular geofences and complex polygon geofences.
Circular Geofences
A circular geofence is defined by a center coordinate and a radius (in meters).
final circular = tl.Geofence(
identifier: 'headquarters',
latitude: 37.7749,
longitude: -122.4194,
radius: 200, // 200 meters
notifyOnEntry: true,
notifyOnExit: true,
notifyOnDwell: true, // Triggers if they stay inside
loiteringDelay: 300, // Trigger dwell after 5 minutes
);Polygon Geofences
A polygon geofence is defined by an array of coordinates outlining a complex shape (like a park or a building).
final polygon = tl.Geofence.polygon(
identifier: 'golden_gate_park',
vertices: [
tl.Coordinate(latitude: 37.773972, longitude: -122.431297),
tl.Coordinate(latitude: 37.769996, longitude: -122.511055),
tl.Coordinate(latitude: 37.764516, longitude: -122.508566),
],
notifyOnEntry: true,
notifyOnExit: true,
);2. Adding and Removing Geofences
Tracelet stores all geofences in the local SQLite database. This means they are persistent across app restarts. You do not need to re-add them every time the app launches.
Adding Geofences
You can add a single geofence or an array of geofences.
// Add a single geofence
await tl.Tracelet.addGeofence(circular);
// Add multiple geofences efficiently
await tl.Tracelet.addGeofences([circular, polygon]);Removing Geofences
Remove them by their unique identifier, or clear the entire database.
// Remove a specific geofence
await tl.Tracelet.removeGeofence('headquarters');
// Remove all geofences entirely
await tl.Tracelet.removeGeofences();3. Listing Active Geofences
You can query the SQLite database at any time to see which geofences are currently being monitored.
final geofences = await tl.Tracelet.getGeofences();
for (final fence in geofences) {
print('Monitoring: \${fence.identifier}');
}4. Listening for Geofence Events
When the user crosses a geofence boundary, Tracelet fires the onGeofence event. Because the evaluation happens in the Rust core during location updates, these events will fire even if the app is killed (triggering your headless callback).
tl.Tracelet.onGeofence((evt) {
if (evt.action == tl.GeofenceEventAction.ENTER) {
print('User entered: \${evt.identifier}');
} else if (evt.action == tl.GeofenceEventAction.EXIT) {
print('User exited: \${evt.identifier}');
} else if (evt.action == tl.GeofenceEventAction.DWELL) {
print('User is dwelling inside: \${evt.identifier}');
}
});5. Android: foreground service & Google Play policy
Google Play policy change — effective October 28, 2026. Geofencing is no
longer a permitted use case for foreground service location. Apps that use a
foreground service solely for geofencing must remove the
FOREGROUND_SERVICE_LOCATION (and FOREGROUND_SERVICE, if unused otherwise)
permissions from their merged manifest.
Tracelet’s standard geofence mode uses the native Android Geofence API
(GeofencingClient), which fires ENTER/EXIT — and relaunches your app into your
headless task — while the app is suspended or terminated, without a
foreground service. Calling startGeofences() in standard mode does not start
a foreground service (as of 3.5.x), so it is compliant by default.
Aggressive OEMs (Samsung/Xiaomi/Huawei/OnePlus/Oppo/Vivo): as of 3.6.1,
an explicit geofenceModeHighAccuracy: false is honored on every device. Before
3.6.1 these OEMs silently forced high-accuracy mode — and with it the
foreground service and its persistent notification — even when you set false,
which broke the compliant standard-mode path above. If you target these devices,
use 3.6.1 or later. On an aggressive OEM the SDK now logs a reliability warning
(native geofence delivery can be delayed there) instead of overriding your config.
If you use Tracelet only for geofencing
-
Do not enable the foreground service:
await tl.Tracelet.ready(const tl.Config( android: tl.AndroidConfig( foregroundService: tl.ForegroundServiceConfig(enabled: false), ), geofence: tl.GeofenceConfig(geofenceModeHighAccuracy: false), )); await tl.Tracelet.startGeofences(); -
The Tracelet SDK declares
FOREGROUND_SERVICE/FOREGROUND_SERVICE_LOCATIONfor its continuous-tracking use case. If your app never does continuous tracking, strip them from the merged manifest with the manifest-mergerremoverule in your app’sandroid/app/src/main/AndroidManifest.xml:<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" tools:node="remove" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove" /> </manifest>
If you use continuous tracking and geofencing
You may keep FOREGROUND_SERVICE_LOCATION for the tracking use case (e.g.
navigation/fleet tracking), but the foreground service must not be used for
geofencing. Tracelet already handles this: the FGS runs for continuous tracking
(start()), and switching to geofence-only mode (startGeofences() in standard
mode) tears the FGS down.
High-accuracy geofence mode (geofenceModeHighAccuracy: true) uses
continuous GPS for tight, in-app boundary detection, which does run a
foreground service. That is continuous location — not “geofencing” in the policy
sense — so only enable it when your app has a separate permitted location use
case. For most apps, standard mode is the compliant and battery-efficient choice.