# How to Add Geofencing Features to Your FlutterFlow App

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-40 min
- Compatibility: FlutterFlow Pro+ (background location and code export recommended for testing)
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
// Custom Action: startGeofenceMonitoring
// Return type: void
// Packages: geolocator, flutter_local_notifications, cloud_firestore

import 'dart:math';

Future<void> startGeofenceMonitoring() async {
  // 1. Initialize local notifications
  final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
  const initSettings = InitializationSettings(
    android: AndroidInitializationSettings('@mipmap/ic_launcher'),
    iOS: DarwinInitializationSettings(),
  );
  await flutterLocalNotificationsPlugin.initialize(initSettings);

  // 2. Request permission (Android 13+)
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.requestNotificationsPermission();

  // 3. Load active geofences from Firestore
  final geofencesSnap = await FirebaseFirestore.instance
      .collection('geofences')
      .where('active', '==', true)
      .get();

  final geofences = geofencesSnap.docs.map((doc) {
    final d = doc.data();
    return {
      'id': doc.id,
      'name': d['name'] as String,
      'lat': (d['centerLat'] as num).toDouble(),
      'lng': (d['centerLng'] as num).toDouble(),
      'radius': (d['radiusMeters'] as num).toDouble(),
    };
  }).toList();

  // 4. Track which zones the user is currently inside
  final Set<String> insideZones = {};

  // 5. Start continuous position stream
  final locationSettings = LocationSettings(
    accuracy: LocationAccuracy.high,
    distanceFilter: 20, // Only fire callback when user moves 20+ meters
  );

  Geolocator.getPositionStream(locationSettings: locationSettings)
      .listen((Position position) async {
    for (final geofence in geofences) {
      final distance = _haversineMeters(
        position.latitude, position.longitude,
        geofence['lat'] as double, geofence['lng'] as double,
      );
      final isInside = distance <= (geofence['radius'] as double);
      final wasInside = insideZones.contains(geofence['id']);

      if (isInside && !wasInside) {
        insideZones.add(geofence['id'] as String);
        await _onGeofenceEvent(
          flutterLocalNotificationsPlugin,
          geofence['id'] as String,
          geofence['name'] as String,
          'entered',
        );
      } else if (!isInside && wasInside) {
        insideZones.remove(geofence['id']);
        await _onGeofenceEvent(
          flutterLocalNotificationsPlugin,
          geofence['id'] as String,
          geofence['name'] as String,
          'exited',
        );
      }
    }
  });
}

double _haversineMeters(double lat1, double lng1, double lat2, double lng2) {
  const R = 6371000.0;
  final dLat = (lat2 - lat1) * pi / 180;
  final dLng = (lng2 - lng1) * pi / 180;
  final a = sin(dLat / 2) * sin(dLat / 2) +
      cos(lat1 * pi / 180) * cos(lat2 * pi / 180) *
      sin(dLng / 2) * sin(dLng / 2);
  return R * 2 * atan2(sqrt(a), sqrt(1 - a));
}

Future<void> _onGeofenceEvent(
  FlutterLocalNotificationsPlugin plugin,
  String zoneId,
  String zoneName,
  String eventType,
) async {
  // Show local notification
  await plugin.show(
    zoneId.hashCode,
    eventType == 'entered' ? 'You arrived at $zoneName' : 'You left $zoneName',
    eventType == 'entered'
        ? 'Welcome to $zoneName!'
        : 'You have exited $zoneName.',
    const NotificationDetails(
      android: AndroidNotificationDetails(
        'geofence_channel',
        'Geofence Alerts',
        importance: Importance.high,
        priority: Priority.high,
      ),
      iOS: DarwinNotificationDetails(),
    ),
  );

  // Log event to Firestore
  final uid = FirebaseAuth.instance.currentUser?.uid;
  if (uid != null) {
    await FirebaseFirestore.instance.collection('geofence_events').add({
      'userId': uid,
      'zoneId': zoneId,
      'zoneName': zoneName,
      'event': eventType,
      'timestamp': FieldValue.serverTimestamp(),
    });
  }
}
```

**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.

```
// Custom Action: requestBackgroundLocation
// Return type: String (permission status)

Future<String> requestBackgroundLocation() async {
  LocationPermission permission = await Geolocator.checkPermission();

  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      return 'denied';
    }
  }

  if (permission == LocationPermission.deniedForever) {
    // Direct user to app settings
    await Geolocator.openAppSettings();
    return 'deniedForever';
  }

  if (permission == LocationPermission.whileInUse) {
    // On iOS, request upgrade to Always
    permission = await Geolocator.requestPermission();
  }

  if (permission == LocationPermission.always) {
    return 'always';
  }

  return 'whileInUse';
}
```

**Expected result:** The action returns 'always' when full background location is granted. Geofence monitoring continues when the app is backgrounded.

## Complete code example

File: `geofencing_complete.dart`

```dart
// ============================================================
// FlutterFlow Geofencing — Complete Custom Action
// ============================================================
// Packages: geolocator, flutter_local_notifications,
// cloud_firestore, firebase_auth, dart:math

import 'dart:math';

// Helper: calculate distance in meters between two GPS points
double haversineMeters(double lat1, double lng1, double lat2, double lng2) {
  const R = 6371000.0;
  final dLat = (lat2 - lat1) * pi / 180;
  final dLng = (lng2 - lng1) * pi / 180;
  final a = sin(dLat / 2) * sin(dLat / 2) +
      cos(lat1 * pi / 180) * cos(lat2 * pi / 180) *
          sin(dLng / 2) * sin(dLng / 2);
  return R * 2 * atan2(sqrt(a), sqrt(1 - a));
}

// Main action: startGeofenceMonitoring
Future<void> startGeofenceMonitoring() async {
  // Initialize local notifications
  final notif = FlutterLocalNotificationsPlugin();
  await notif.initialize(
    const InitializationSettings(
      android: AndroidInitializationSettings('@mipmap/ic_launcher'),
      iOS: DarwinInitializationSettings(),
    ),
  );

  // Load geofences from Firestore
  final snap = await FirebaseFirestore.instance
      .collection('geofences')
      .where('active', '==', true)
      .get();

  final zones = snap.docs.map((d) => {
        'id': d.id,
        'name': d['name'] as String,
        'lat': (d['centerLat'] as num).toDouble(),
        'lng': (d['centerLng'] as num).toDouble(),
        'radius': (d['radiusMeters'] as num).toDouble(),
      }).toList();

  final Set<String> insideZones = {};

  // Start GPS stream with 20-meter filter
  Geolocator.getPositionStream(
    locationSettings: const LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 20,
    ),
  ).listen((pos) async {
    for (final z in zones) {
      final dist = haversineMeters(
          pos.latitude, pos.longitude,
          z['lat'] as double, z['lng'] as double);
      final inside = dist <= (z['radius'] as double);
      final was = insideZones.contains(z['id']);

      String? event;
      if (inside && !was) {
        insideZones.add(z['id'] as String);
        event = 'entered';
      } else if (!inside && was) {
        insideZones.remove(z['id']);
        event = 'exited';
      }

      if (event != null) {
        final title = event == 'entered'
            ? 'Arrived at ${z['name']}'
            : 'Left ${z['name']}';
        await notif.show(
          (z['id'] as String).hashCode, title, '',
          const NotificationDetails(
            android: AndroidNotificationDetails(
                'geofence', 'Geofence Alerts',
                importance: Importance.high),
            iOS: DarwinNotificationDetails(),
          ),
        );
        final uid = FirebaseAuth.instance.currentUser?.uid;
        if (uid != null) {
          await FirebaseFirestore.instance
              .collection('geofence_events')
              .add({
            'userId': uid,
            'zoneId': z['id'],
            'event': event,
            'timestamp': FieldValue.serverTimestamp(),
          });
        }
      }
    }
  });
}
```

## Common mistakes

- **Not requesting 'Always Allow' location permission — only requesting 'While Using'** — When the user switches to another app or locks their phone, the geolocator position stream pauses if only 'While Using' is granted. Geofence entry and exit events that occur while the app is backgrounded are missed entirely. Fix: 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.
- **Setting distanceFilter to 0 (or not setting it), causing hundreds of position updates per minute** — Without a distanceFilter, the GPS stream fires on every position fix from the hardware, which can be multiple times per second when stationary due to GPS jitter. This floods your Firestore with unnecessary writes, drains the battery rapidly, and executes your geofence check code hundreds of times per minute. Fix: 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.
- **Defining geofence zones smaller than 100 meters radius** — GPS accuracy on mobile phones varies from 5m in open sky to 100m+ in urban environments with tall buildings, indoors, or in parking garages. Zones smaller than 100m will generate constant false enter/exit events as GPS accuracy fluctuates around the boundary. Fix: 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.
- **Re-sending the 'entered' notification every position update while the user is inside a zone** — Without tracking the 'inside/outside' state, every GPS update while the user is inside a geofence triggers a new 'You entered...' notification. The user receives dozens of identical notifications in minutes. Fix: 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.

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-add-geofencing-features-to-your-flutterflow-app
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-add-geofencing-features-to-your-flutterflow-app
