Geofencing in FlutterFlow uses the geolocator package to continuously monitor the user's GPS position and compare it against defined boundary zones stored in Firestore. When the device detects an entry or exit from a zone, a Custom Action fires a local notification and logs the event to Firestore. A Google Maps custom widget can display the geofence boundaries as circle overlays.
Client-Side Geofencing with Continuous Position Monitoring
Geofencing means 'do something when a user enters or leaves a defined geographic area'. There are two implementation strategies: client-side (the device monitors its own position) and server-side (a backend checks all users' positions). This tutorial covers client-side geofencing using the geolocator package, which is simpler to implement and does not require a backend subscription. The key challenge is background location — the device must continue tracking position even when the app is not in the foreground.
Prerequisites
- FlutterFlow project with Firebase connected (Firestore for storing geofences and events)
- Basic understanding of Custom Actions and App State in FlutterFlow
- A physical device for testing (geolocation does not work reliably in emulators)
- Location permission handling experience or willingness to follow this tutorial's steps
Step-by-step guide
Add Required Packages and Configure Permissions
Add Required Packages and Configure Permissions
In FlutterFlow, go to Settings (gear icon, top-right) → Pubspec Dependencies. Search for and enable: 'geolocator' (GPS monitoring), 'flutter_local_notifications' (on-device notifications without FCM), and 'dart_geohash' (optional, for efficient proximity queries). Next, go to Settings → App Details → Permissions. Enable 'Location - While Using App' AND 'Location - Always (Background)'. FlutterFlow adds the required strings to Info.plist (iOS) and AndroidManifest.xml (Android) automatically. On Android, also enable 'Notifications' permission. On iOS, Background Modes must include 'Location updates' — FlutterFlow handles this when you enable background location.
Expected result: The pubspec.yaml includes geolocator and flutter_local_notifications. Location permission strings are visible in Settings → Permissions.
Create the Geofences Collection in Firestore
Create the Geofences Collection in Firestore
In Firebase Console, create a 'geofences' collection. Each document represents one geographic zone. Add the following fields: 'name' (String), 'centerLat' (Number), 'centerLng' (Number), 'radiusMeters' (Number), 'type' (String — e.g., 'store', 'office', 'restricted'), and 'active' (Boolean). Create a sample zone: name='Downtown Office', centerLat=37.7749, centerLng=-122.4194, radiusMeters=150, active=true. In FlutterFlow's Firestore schema editor, add this collection and data type. You can build an admin screen in FlutterFlow to manage geofences, or manage them directly in the Firebase Console.
Expected result: The Firestore 'geofences' collection has at least one active document with center coordinates and a radius in meters.
Create the StartGeofenceMonitoring Custom Action
Create the StartGeofenceMonitoring Custom Action
In Custom Code → Custom Actions → '+', create 'startGeofenceMonitoring'. This action initializes the local notification plugin, loads all active geofences from Firestore, then starts a position stream using Geolocator.getPositionStream(). For each position update, the action checks if the user has entered or exited any geofence by calculating the Haversine distance from the user's position to each zone center. It maintains an in-memory set of 'currently inside' zone IDs and fires a callback when the state changes (was outside, now inside = 'entered'; was inside, now outside = 'exited'). Wire this to your home page's On Page Load action.
1// Custom Action: startGeofenceMonitoring2// Return type: void3// Packages: geolocator, flutter_local_notifications, cloud_firestore45import 'dart:math';67Future<void> startGeofenceMonitoring() async {8 // 1. Initialize local notifications9 final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();10 const initSettings = InitializationSettings(11 android: AndroidInitializationSettings('@mipmap/ic_launcher'),12 iOS: DarwinInitializationSettings(),13 );14 await flutterLocalNotificationsPlugin.initialize(initSettings);1516 // 2. Request permission (Android 13+)17 await flutterLocalNotificationsPlugin18 .resolvePlatformSpecificImplementation<19 AndroidFlutterLocalNotificationsPlugin>()20 ?.requestNotificationsPermission();2122 // 3. Load active geofences from Firestore23 final geofencesSnap = await FirebaseFirestore.instance24 .collection('geofences')25 .where('active', '==', true)26 .get();2728 final geofences = geofencesSnap.docs.map((doc) {29 final d = doc.data();30 return {31 'id': doc.id,32 'name': d['name'] as String,33 'lat': (d['centerLat'] as num).toDouble(),34 'lng': (d['centerLng'] as num).toDouble(),35 'radius': (d['radiusMeters'] as num).toDouble(),36 };37 }).toList();3839 // 4. Track which zones the user is currently inside40 final Set<String> insideZones = {};4142 // 5. Start continuous position stream43 final locationSettings = LocationSettings(44 accuracy: LocationAccuracy.high,45 distanceFilter: 20, // Only fire callback when user moves 20+ meters46 );4748 Geolocator.getPositionStream(locationSettings: locationSettings)49 .listen((Position position) async {50 for (final geofence in geofences) {51 final distance = _haversineMeters(52 position.latitude, position.longitude,53 geofence['lat'] as double, geofence['lng'] as double,54 );55 final isInside = distance <= (geofence['radius'] as double);56 final wasInside = insideZones.contains(geofence['id']);5758 if (isInside && !wasInside) {59 insideZones.add(geofence['id'] as String);60 await _onGeofenceEvent(61 flutterLocalNotificationsPlugin,62 geofence['id'] as String,63 geofence['name'] as String,64 'entered',65 );66 } else if (!isInside && wasInside) {67 insideZones.remove(geofence['id']);68 await _onGeofenceEvent(69 flutterLocalNotificationsPlugin,70 geofence['id'] as String,71 geofence['name'] as String,72 'exited',73 );74 }75 }76 });77}7879double _haversineMeters(double lat1, double lng1, double lat2, double lng2) {80 const R = 6371000.0;81 final dLat = (lat2 - lat1) * pi / 180;82 final dLng = (lng2 - lng1) * pi / 180;83 final a = sin(dLat / 2) * sin(dLat / 2) +84 cos(lat1 * pi / 180) * cos(lat2 * pi / 180) *85 sin(dLng / 2) * sin(dLng / 2);86 return R * 2 * atan2(sqrt(a), sqrt(1 - a));87}8889Future<void> _onGeofenceEvent(90 FlutterLocalNotificationsPlugin plugin,91 String zoneId,92 String zoneName,93 String eventType,94) async {95 // Show local notification96 await plugin.show(97 zoneId.hashCode,98 eventType == 'entered' ? 'You arrived at $zoneName' : 'You left $zoneName',99 eventType == 'entered'100 ? 'Welcome to $zoneName!'101 : 'You have exited $zoneName.',102 const NotificationDetails(103 android: AndroidNotificationDetails(104 'geofence_channel',105 'Geofence Alerts',106 importance: Importance.high,107 priority: Priority.high,108 ),109 iOS: DarwinNotificationDetails(),110 ),111 );112113 // Log event to Firestore114 final uid = FirebaseAuth.instance.currentUser?.uid;115 if (uid != null) {116 await FirebaseFirestore.instance.collection('geofence_events').add({117 'userId': uid,118 'zoneId': zoneId,119 'zoneName': zoneName,120 'event': eventType,121 'timestamp': FieldValue.serverTimestamp(),122 });123 }124}Expected result: The action starts a background location stream. Moving into a geofence zone triggers a local notification and creates a document in the Firestore 'geofence_events' collection.
Display Geofence Zones on a Map
Display Geofence Zones on a Map
To visualize geofence boundaries on a map, create a Custom Widget using the google_maps_flutter package. The widget takes a list of geofence data (center coordinates + radius) and renders Google Map Circle overlays — semi-transparent colored circles drawn at each zone's location. In FlutterFlow, add google_maps_flutter in Settings → Pubspec Dependencies. Create a Custom Widget named 'GeofenceMap'. Pass a list of geofence maps as a parameter. Inside the widget, use GoogleMap with circles: a Set of Circle objects built from your geofences list. Each Circle has a circleId, center LatLng, radius in meters, fill color (semi-transparent), and stroke color. Drag the GeofenceMap widget onto your map page and bind the geofences parameter to a Backend Query result.
Expected result: The map page shows geofence boundaries as colored circle overlays. The user's current location dot is visible inside or outside the circles.
Request Background Location Permission at the Right Time
Request Background Location Permission at the Right Time
iOS and Android both require you to explain WHY you need background location before the system dialog appears. Never request background location on first app launch without context. Instead, create an 'Enable Geofencing' screen with a clear explanation (e.g., 'Allow background location so we can notify you when you arrive at saved locations'). Add a button that triggers a Custom Action requesting LocationPermission.always on iOS. On Android 10+, the system shows the permission dialog automatically when you request background location — guide users to tap 'Allow all the time' instead of 'Allow only while using'. After permission is granted, call startGeofenceMonitoring.
1// Custom Action: requestBackgroundLocation2// Return type: String (permission status)34Future<String> requestBackgroundLocation() async {5 LocationPermission permission = await Geolocator.checkPermission();67 if (permission == LocationPermission.denied) {8 permission = await Geolocator.requestPermission();9 if (permission == LocationPermission.denied) {10 return 'denied';11 }12 }1314 if (permission == LocationPermission.deniedForever) {15 // Direct user to app settings16 await Geolocator.openAppSettings();17 return 'deniedForever';18 }1920 if (permission == LocationPermission.whileInUse) {21 // On iOS, request upgrade to Always22 permission = await Geolocator.requestPermission();23 }2425 if (permission == LocationPermission.always) {26 return 'always';27 }2829 return 'whileInUse';30}Expected result: The action returns 'always' when full background location is granted. Geofence monitoring continues when the app is backgrounded.
Complete working example
1// ============================================================2// FlutterFlow Geofencing — Complete Custom Action3// ============================================================4// Packages: geolocator, flutter_local_notifications,5// cloud_firestore, firebase_auth, dart:math67import 'dart:math';89// Helper: calculate distance in meters between two GPS points10double haversineMeters(double lat1, double lng1, double lat2, double lng2) {11 const R = 6371000.0;12 final dLat = (lat2 - lat1) * pi / 180;13 final dLng = (lng2 - lng1) * pi / 180;14 final a = sin(dLat / 2) * sin(dLat / 2) +15 cos(lat1 * pi / 180) * cos(lat2 * pi / 180) *16 sin(dLng / 2) * sin(dLng / 2);17 return R * 2 * atan2(sqrt(a), sqrt(1 - a));18}1920// Main action: startGeofenceMonitoring21Future<void> startGeofenceMonitoring() async {22 // Initialize local notifications23 final notif = FlutterLocalNotificationsPlugin();24 await notif.initialize(25 const InitializationSettings(26 android: AndroidInitializationSettings('@mipmap/ic_launcher'),27 iOS: DarwinInitializationSettings(),28 ),29 );3031 // Load geofences from Firestore32 final snap = await FirebaseFirestore.instance33 .collection('geofences')34 .where('active', '==', true)35 .get();3637 final zones = snap.docs.map((d) => {38 'id': d.id,39 'name': d['name'] as String,40 'lat': (d['centerLat'] as num).toDouble(),41 'lng': (d['centerLng'] as num).toDouble(),42 'radius': (d['radiusMeters'] as num).toDouble(),43 }).toList();4445 final Set<String> insideZones = {};4647 // Start GPS stream with 20-meter filter48 Geolocator.getPositionStream(49 locationSettings: const LocationSettings(50 accuracy: LocationAccuracy.high,51 distanceFilter: 20,52 ),53 ).listen((pos) async {54 for (final z in zones) {55 final dist = haversineMeters(56 pos.latitude, pos.longitude,57 z['lat'] as double, z['lng'] as double);58 final inside = dist <= (z['radius'] as double);59 final was = insideZones.contains(z['id']);6061 String? event;62 if (inside && !was) {63 insideZones.add(z['id'] as String);64 event = 'entered';65 } else if (!inside && was) {66 insideZones.remove(z['id']);67 event = 'exited';68 }6970 if (event != null) {71 final title = event == 'entered'72 ? 'Arrived at ${z['name']}'73 : 'Left ${z['name']}';74 await notif.show(75 (z['id'] as String).hashCode, title, '',76 const NotificationDetails(77 android: AndroidNotificationDetails(78 'geofence', 'Geofence Alerts',79 importance: Importance.high),80 iOS: DarwinNotificationDetails(),81 ),82 );83 final uid = FirebaseAuth.instance.currentUser?.uid;84 if (uid != null) {85 await FirebaseFirestore.instance86 .collection('geofence_events')87 .add({88 'userId': uid,89 'zoneId': z['id'],90 'event': event,91 'timestamp': FieldValue.serverTimestamp(),92 });93 }94 }95 }96 });97}Common mistakes when adding Geofencing Features to Your FlutterFlow App
Why it's a problem: Not requesting 'Always Allow' location permission — only requesting 'While Using'
How to avoid: Request LocationPermission.always on iOS and 'Allow all the time' on Android. Show a clear explanation screen before requesting this permission. Use Geolocator.checkPermission() to verify the granted level and show a warning if only 'While Using' was granted.
Why it's a problem: Setting distanceFilter to 0 (or not setting it), causing hundreds of position updates per minute
How to avoid: Set distanceFilter to at least 20 meters. This means the stream only fires when the user has actually moved 20+ meters from the last fix, dramatically reducing callbacks and battery usage while still being responsive for zone entry/exit detection.
Why it's a problem: Defining geofence zones smaller than 100 meters radius
How to avoid: Use a minimum radius of 100 meters for outdoor zones and 200+ meters for urban environments with potential GPS interference. Design your app's value proposition around zones that make sense at this scale — city blocks, neighborhoods, landmarks.
Why it's a problem: Re-sending the 'entered' notification every position update while the user is inside a zone
How to avoid: Maintain a Set of currently-inside zone IDs. Only fire the 'entered' event when transitioning from outside to inside (was not in set, now in set). Only fire 'exited' when transitioning from inside to outside. This is handled by the insideZones Set in this tutorial's implementation.
Best practices
- Always explain background location use with a clear permission screen before the system dialog — iOS rejects apps that request location without visible UI justification.
- Set distanceFilter to 20-50 meters to balance responsiveness and battery life — lower values drain battery faster.
- Store geofence events in Firestore with timestamps so you have a history of each user's zone interactions for analytics and debugging.
- Add a user-facing toggle for geofencing in your app settings — some users do not want to be tracked even if they initially allowed it.
- Test geofencing on physical hardware in the actual locations if possible — use a car or bike ride past your test geofence zone to verify real-world triggering.
- Implement debouncing: if a user crosses a boundary multiple times in 60 seconds (hovering on the edge), send only one notification per minute per zone.
- Use Google Maps Circle overlays to show users where their geofence zones are — transparency builds trust and reduces support requests about unexpected notifications.
- Log background location access to your privacy policy — most jurisdictions require disclosure of background location data collection.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a geofencing feature in a FlutterFlow app using the geolocator package. I have geofence zones stored in Firestore with centerLat, centerLng, and radiusMeters fields. Write a Dart function that monitors the device's GPS position stream, calculates Haversine distance to each zone, and fires separate callbacks for 'entered' and 'exited' events while avoiding duplicate triggers.
Write a FlutterFlow Custom Action in Dart called startGeofenceMonitoring that: loads active geofences from a Firestore 'geofences' collection, starts a geolocator position stream with distanceFilter 20, checks each position update against all zone radii using Haversine distance, and shows a flutter_local_notifications alert on zone entry or exit.
Frequently asked questions
Does geofencing work when the app is completely closed on iOS?
Standard geolocator position streams stop when the app is killed. For truly background-and-closed geofencing on iOS, you need a native plugin that uses CLRegion monitoring (iOS's native geofencing API), which works even when the app is not running. Alternatively, use the server-side Cloud Function approach from the 'Push Notifications Based on User Location' tutorial — it works regardless of whether the app is open.
How many geofence zones can the app monitor simultaneously?
The geolocator approach in this tutorial supports unlimited zones (you check all zones in the position stream callback). However, iOS's native CLRegion monitoring (used by some plugins) is limited to 20 zones per app. The geolocator stream approach avoids this limit but requires the app to be running in the background.
How much battery does continuous geofencing drain?
With LocationAccuracy.high and distanceFilter: 20, expect roughly 5-8% additional battery drain per hour on modern phones. Using LocationAccuracy.medium reduces this to 2-4% per hour by using network positioning instead of GPS. For less critical geofencing, consider polling every 5 minutes instead of continuous streaming to reduce battery impact to under 1% per hour.
Can I draw the geofence circles on a Google Map in FlutterFlow?
Yes, but it requires a Custom Widget using the google_maps_flutter package. FlutterFlow's built-in Map widget does not support Circle overlays. In the Custom Widget, create a Set of Circle objects with LatLng center, radius in meters, fillColor (semi-transparent), and strokeColor, then pass them to the GoogleMap widget's 'circles' parameter. Step 4 of this tutorial covers the approach.
What is the difference between geolocator position stream and native geofencing APIs?
The geolocator stream requires your app to be actively running (foreground or background). Native geofencing APIs (iOS CLRegion, Android Geofencing API) are managed by the OS and fire events even when the app is killed. Native is more battery-efficient and reliable, but requires more complex setup and platform-specific code. For most FlutterFlow use cases, the geolocator stream approach in this tutorial is the practical starting point.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation