-
Notifications
You must be signed in to change notification settings - Fork 257
Description
Required Reading
- Confirmed
Plugin Version
4.16.11
Mobile operating-system(s)
- iOS
- Android
Device Manufacturer(s) and Model(s)
Samsung Galaxy s22, Iphone 15 Pro
Device operating-systems(s)
iOS 18.5, Android 15
What do you require assistance about?
We have purchased the paid version of the flutter_background_geolocation package and integrated it into our Flutter app.
We are using the package strictly for geofencing purposes, with the following requirements:
Geofence events must trigger in foreground, background, and terminated states.
Events must trigger with or without internet.
The geofence zones represent (e.g., building).
Upon entering/exiting a zone, we execute several functions, such as sending notifications, writing to Hive DB, api call, and calling native method channels.
Usage Flow
- User logs in, and we fetch geofence zones from the backend.
- Request required permissions.
- Start a listener for geofence events.
- Register a headless task for terminated state handling on android.
- Dynamically add/remove geofences from the device.
- Start the package using a tailored config.
- Initial triggered using geolocation, current location and turf to see if user in geofence or not.
Questions & Support Needed
1- In real-world testing, we noticed significant battery drain. For example, the device dropped from 30% to 9% in 3 hours with no other app usage. Could you help us optimize our configuration to reduce battery impact?
2- We do not require the package to make any HTTP requests. Can we safely disable all related sync/configs?
3- Your documentation mentions that iOS can relaunch the app in the background when a geofence event occurs in the terminated state. When this happens: Does the full app lifecycle run (e.g., main(), initState() etc.)? Where exactly should we place our logic to ensure our onGeofenceTriggered function runs correctly?
4- Could you kindly review the configuration (shared in startGeofenceService) and advise any improvements specifically for: 24/7 geofence monitoring (foreground/background/terminated) and Only geofencing
5- Is it possible to suppress the persistent notification shown when geolocation service runs in the on Android?
6- If the user disables location services while the app is terminated, can we detect this condition when the app wakes via provider change? and trigger a custom notification and execute a function (like we do for geofence events)?
7- When the user restarts the phone, we want to trigger the event again. How can we handle that?
8- Does this package have features like recurring rules?. Triggered event only if those recurring rules match?
[Optional] Plugin Code and/or Config
🔄 Usage Flow with code
1- User logs in, and we fetch geofence zones from the backend.
2- Request required permissions.
3- Start a listener for geofence events.
///Start live location updates
void startLiveLocationUpdates(List<GeoJsonArea> areas) {
_backgroundGeolocation.onGeofence(
(geofenceEvent) async {
final rrule = geofenceEvent.extras?['recurringRule'] as String?;
if (rrule != null) {
var trigger = false;
// If the event is enter, check if the time matches the recurring rule
if (geofenceEvent.action.event == GeoFenceEvent.enter) {
trigger = rrule.check();
}
if (trigger) {
final event = GeoFenseEventTrigger(
event: geofenceEvent.action.event,
name: geofenceEvent.identifier,
coordinate: Coordinate(
longitude: geofenceEvent.location.coords.longitude,
latitude: geofenceEvent.location.coords.latitude,
),
time: DateTime.now(),
);
_lastEvent = event;
await _geoFencingForegroundRepository.onGenfenceTriggered(
event,
);
}
}
},
);
}
4- Register a headless task for terminated state handling on android.
@pragma('vm:entry-point')
Future<void> backgroundGeolocationHeadlessTask(
bg.HeadlessEvent headlessEvent,
) async {
switch (headlessEvent.name) {
case bg.Event.GEOFENCE:
final geofenceEvent = headlessEvent.event as bg.GeofenceEvent;
try {
final rrule = geofenceEvent.extras?['recurringRule'] as String?;
if (rrule != null) {
if (geofenceEvent.action.event == GeoFenceEvent.enter) {
final isMatch = rrule.check();
if (isMatch) {
await onGeofenceTriggered(geofenceEvent);
}
} else {
await onGeofenceTriggered(geofenceEvent);
}
}
} catch (e) {
_log.shoutIfEnabled(() => 'Error in geofence event: $e');
debugPrint('Error in geofence event: $e');
}
case bg.Event.PROVIDERCHANGE:
final providerChangeEvent = headlessEvent.event as bg.ProviderChangeEvent;
if (providerChangeEvent.gpsOff()) {
try {
await backgroundContainerInit();
await globalContainer
.read(localNotificationServiceProvider)
.displayNotification(
id: notificationPermissionRemovedId,
title: TextConstants.notificationPermissionRemovedTitle,
body: TextConstants.notificationPermissionRemovedBody,
payload: 'location_services_disabled',
);
} catch (e) {
_log.shoutIfEnabled(() => 'Error in provider change event: $e');
debugPrint('Error in provider change event: $e');
}
}
}
}
5- Dynamically add/remove geofences from the device.
Future<void> init(List<GeoJsonArea> areas) async {
try {
final alreadyAddedZones = await _backgroundGeolocation.geofences;
final areasToAdd = areas.areasToAddToGeoFense(alreadyAddedZones);
final areasToRemove = areas.areasToRemoveFromGeoFense(alreadyAddedZones);
await _removeGeofenceZones(areasToRemove);
await _addGeofenceZones(areasToAdd);
_log.infoIfEnabled(
() => 'GeoFencingForegroundService init completed',
);
} catch (e) {
_log.shoutIfEnabled(
e.toString,
);
}
}
6- Start the package using a tailored config.
Future<void> startGeofenceService() async {
try {
final configuration = _applicationconfigurationProvider
.getJson(ConfigurationKeys.localNotification);
final groupChannelId = configuration.getAndEnsure('group_channel_id');
final groupChannelName = configuration.getAndEnsure('group_channel_name');
final state = await _backgroundGeolocation.ready(
bg.Config(
notification: bg.Notification(
channelName: groupChannelName,
channelId: groupChannelId,
smallIcon: 'mipmap/ic_launcher',
title: TextConstants.notificationMainTitle,
priority: bg.Config.NOTIFICATION_PRIORITY_MIN,
),
locationAuthorizationAlert: {
'titleWhenNotEnabled': TextConstants.locationServicesDisabled,
'titleWhenOff': TextConstants.locationServicesDisabled,
'instructions': TextConstants.pleaseEnableLocationServices,
'cancelButton': TextConstants.cancel,
'settingsButton': TextConstants.settings,
},
autoSync: true,
geofenceModeHighAccuracy: true,
desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
distanceFilter: 10,
debug: AppRunnerMode.instance.isDebugMode,
logLevel: bg.Config.LOG_LEVEL_VERBOSE,
stopTimeout: 5,
geofenceProximityRadius: 1000,
locationAuthorizationRequest: 'Always',
enableHeadless: true,
heartbeatInterval: 60,
stopOnTerminate: false,
startOnBoot: true,
geofenceInitialTriggerEntry: true,
reset: true,
isMoving: true,
showsBackgroundLocationIndicator: false,
),
);
if (!state.enabled) {
await _backgroundGeolocation.startGeofences();
}
} catch (e) {
debugPrint('Error: $e');
}
}
7- Initial triggered using geolocation current location and turf to see if user in geofence or not
Future<void> initialTriggered(List<GeoJsonArea> areas) async {
try {
// using geolocator
final location = await _backgroundGeolocation.getCurrentPosition();
final insideArea = areas.currentGeoFence(
location.latitude,
location.longitude,
);
if (insideArea == null) {
return;
}
final rrule = insideArea.recurringRule;
if (rrule != null) {
final isMatch = rrule.check();
if (isMatch) {
_lastEvent = GeoFenseEventTrigger(
event: GeoFenceEvent.enter,
name: insideArea.name ?? '',
coordinate: Coordinate(
longitude: location.longitude,
latitude: location.latitude,
),
time: DateTime.now(),
);
await _geoFencingForegroundRepository.onGenfenceTriggered(
_lastEvent!,
);
}
}
} catch (e) {
_log.shoutIfEnabled(() => 'Initial Location Trigger Error: $e');
}
}
Ios Info.plist
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>fetch</string>
<string>processing</string>
</array>