Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Add Geofencing Features to Your FlutterFlow App

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.

What you'll learn

  • How to request background location permission ('Always Allow') for reliable geofencing
  • How to define geofence zones in Firestore and load them into your Custom Action
  • How to calculate if a user is inside a zone using Haversine distance in a Custom Action
  • How to trigger local push notifications and log Firestore events on geofence enter/exit
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner12 min read30-40 minFlutterFlow Pro+ (background location and code export recommended for testing)March 2026RapidDev Engineering Team
TL;DR

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

1

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.

2

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.

3

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.

start_geofence_monitoring.dart
1// Custom Action: startGeofenceMonitoring
2// Return type: void
3// Packages: geolocator, flutter_local_notifications, cloud_firestore
4
5import 'dart:math';
6
7Future<void> startGeofenceMonitoring() async {
8 // 1. Initialize local notifications
9 final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
10 const initSettings = InitializationSettings(
11 android: AndroidInitializationSettings('@mipmap/ic_launcher'),
12 iOS: DarwinInitializationSettings(),
13 );
14 await flutterLocalNotificationsPlugin.initialize(initSettings);
15
16 // 2. Request permission (Android 13+)
17 await flutterLocalNotificationsPlugin
18 .resolvePlatformSpecificImplementation<
19 AndroidFlutterLocalNotificationsPlugin>()
20 ?.requestNotificationsPermission();
21
22 // 3. Load active geofences from Firestore
23 final geofencesSnap = await FirebaseFirestore.instance
24 .collection('geofences')
25 .where('active', '==', true)
26 .get();
27
28 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();
38
39 // 4. Track which zones the user is currently inside
40 final Set<String> insideZones = {};
41
42 // 5. Start continuous position stream
43 final locationSettings = LocationSettings(
44 accuracy: LocationAccuracy.high,
45 distanceFilter: 20, // Only fire callback when user moves 20+ meters
46 );
47
48 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']);
57
58 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}
78
79double _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}
88
89Future<void> _onGeofenceEvent(
90 FlutterLocalNotificationsPlugin plugin,
91 String zoneId,
92 String zoneName,
93 String eventType,
94) async {
95 // Show local notification
96 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 );
112
113 // Log event to Firestore
114 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.

4

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.

5

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.

request_background_location.dart
1// Custom Action: requestBackgroundLocation
2// Return type: String (permission status)
3
4Future<String> requestBackgroundLocation() async {
5 LocationPermission permission = await Geolocator.checkPermission();
6
7 if (permission == LocationPermission.denied) {
8 permission = await Geolocator.requestPermission();
9 if (permission == LocationPermission.denied) {
10 return 'denied';
11 }
12 }
13
14 if (permission == LocationPermission.deniedForever) {
15 // Direct user to app settings
16 await Geolocator.openAppSettings();
17 return 'deniedForever';
18 }
19
20 if (permission == LocationPermission.whileInUse) {
21 // On iOS, request upgrade to Always
22 permission = await Geolocator.requestPermission();
23 }
24
25 if (permission == LocationPermission.always) {
26 return 'always';
27 }
28
29 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

geofencing_complete.dart
1// ============================================================
2// FlutterFlow Geofencing — Complete Custom Action
3// ============================================================
4// Packages: geolocator, flutter_local_notifications,
5// cloud_firestore, firebase_auth, dart:math
6
7import 'dart:math';
8
9// Helper: calculate distance in meters between two GPS points
10double 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}
19
20// Main action: startGeofenceMonitoring
21Future<void> startGeofenceMonitoring() async {
22 // Initialize local notifications
23 final notif = FlutterLocalNotificationsPlugin();
24 await notif.initialize(
25 const InitializationSettings(
26 android: AndroidInitializationSettings('@mipmap/ic_launcher'),
27 iOS: DarwinInitializationSettings(),
28 ),
29 );
30
31 // Load geofences from Firestore
32 final snap = await FirebaseFirestore.instance
33 .collection('geofences')
34 .where('active', '==', true)
35 .get();
36
37 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();
44
45 final Set<String> insideZones = {};
46
47 // Start GPS stream with 20-meter filter
48 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']);
60
61 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 }
69
70 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.instance
86 .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.

ChatGPT Prompt

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.

FlutterFlow Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.