Implementing Geofencing to Trigger Actions When Users Enter or Exit Specific Areas in Flutter

Geofencing is a powerful feature that allows mobile applications to be aware of a user’s location and trigger specific actions when they enter or exit predefined geographical boundaries, known as geofences. This functionality can be highly beneficial in various scenarios, such as sending notifications when a user arrives at a store, triggering smart home automations when they get near their house, or tracking attendance in a school setting. This comprehensive guide explains how to implement geofencing in Flutter to trigger actions when users enter or exit specific areas.

What is Geofencing?

Geofencing uses location services to define virtual perimeters around real-world geographical areas. When a mobile device enters or exits these predefined zones, the application receives a notification and can trigger relevant actions. This feature is commonly used for location-based marketing, security, and automation.

Why Implement Geofencing in Flutter?

  • Location-Based Functionality: Enhance user experience by providing location-aware features.
  • Targeted Notifications: Send relevant notifications based on the user’s proximity to specific locations.
  • Automation: Trigger automated tasks when users enter or exit predefined areas.
  • Security: Monitor user movement within or outside specified zones for security purposes.

Prerequisites

Before diving into the implementation, make sure you have the following prerequisites in place:

  • Flutter SDK: Ensure that you have Flutter SDK installed and set up correctly.
  • Dart SDK: Dart comes bundled with Flutter, so ensure it’s correctly configured.
  • Android Studio/Xcode: Use either Android Studio for Android or Xcode for iOS development.
  • Flutter Project: Create a new Flutter project or use an existing one.
  • Required Packages: Add the necessary packages to your pubspec.yaml file.

Adding Dependencies

You need to add the geolocator and geofence_service packages to your pubspec.yaml file. These packages will help you with location services and geofencing functionalities.

dependencies:
  flutter:
    sdk: flutter
  geolocator: ^9.0.2
  geofence_service: ^4.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

After adding the dependencies, run flutter pub get to install them.

Implementation Steps

Follow these steps to implement geofencing in your Flutter application:

Step 1: Setting up the Background Service

To handle geofence transitions, you need to set up a background service. The geofence_service package provides tools to create and manage this service. Start by creating a new Dart file (e.g., geofence_handler.dart) to define the service.

import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:geofence_service/geofence_service.dart';
import 'package:geolocator/geolocator.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Register the UI isolate's dispatcher
  DartPluginRegistrant.ensureInitialized();

  // Plugin initialization
  GeofenceService.initialize(
      backgroundCallback: backgroundCallback,
      foregroundCallback: foregroundCallback);
}

// Callback function for foreground events
void foregroundCallback(Location location, Geofence geofence) {
  print('Foreground: Location: $location, Geofence: $geofence');
}

// Callback function for background events
void backgroundCallback(Location location, Geofence geofence) {
  print('Background: Location: $location, Geofence: $geofence');
}

Step 2: Defining Geofences

Define the geofences that you want to monitor. A geofence consists of a latitude, longitude, radius, and an identifier. You can define multiple geofences based on your application’s needs.

import 'package:geofence_service/geofence_service.dart';

void defineGeofences() {
  final geofenceList = [
    Geofence(
      id: 'Home',
      latitude: 37.7749, // Replace with your latitude
      longitude: -122.4194, // Replace with your longitude
      radius: [
        const RadiusDescriptor(id: 'inner', length: 50),
        const RadiusDescriptor(id: 'outer', length: 100),
      ],
    ),
    Geofence(
      id: 'Work',
      latitude: 34.0522, // Replace with your latitude
      longitude: -118.2437, // Replace with your longitude
      radius: [
        const RadiusDescriptor(id: 'inner', length: 50),
        const RadiusDescriptor(id: 'outer', length: 100),
      ],
    ),
  ];
}

Step 3: Starting the Geofence Service

In your Flutter application, start the geofence service and register the defined geofences. Ensure that you have requested the necessary permissions (location permission) before starting the service.

import 'package:flutter/material.dart';
import 'package:geofence_service/geofence_service.dart';
import 'package:permission_handler/permission_handler.dart';

class GeofenceScreen extends StatefulWidget {
  @override
  _GeofenceScreenState createState() => _GeofenceScreenState();
}

class _GeofenceScreenState extends State<GeofenceScreen> {
  final geofenceService = GeofenceService.instance;
  bool _isServiceRunning = false;

  final geofenceList = [
    Geofence(
      id: 'Home',
      latitude: 37.7749, // Replace with your latitude
      longitude: -122.4194, // Replace with your longitude
      radius: [
        const RadiusDescriptor(id: 'inner', length: 50),
        const RadiusDescriptor(id: 'outer', length: 100),
      ],
    ),
    Geofence(
      id: 'Work',
      latitude: 34.0522, // Replace with your latitude
      longitude: -118.2437, // Replace with your longitude
      radius: [
        const RadiusDescriptor(id: 'inner', length: 50),
        const RadiusDescriptor(id: 'outer', length: 100),
      ],
    ),
  ];

  Future<void> _startGeofenceService() async {
    // Check and request permissions
    var locationStatus = await Permission.locationAlways.status;
    if (!locationStatus.isGranted) {
      await Permission.locationAlways.request();
    }

    var backgroundStatus = await Permission.locationAlways.status;
    if (!backgroundStatus.isGranted) {
      await Permission.locationAlways.request();
    }

    geofenceService.addGeofenceStatusStream((geofence, geofenceStatus, location) {
      print('Geofence: $geofence, Status: $geofenceStatus, Location: $location');
    });

    geofenceService.start(geofenceList).catchError((error) {
      print('Error starting GeofenceService: $error');
    });

    setState(() {
      _isServiceRunning = true;
    });
  }

  Future<void> _stopGeofenceService() async {
    geofenceService.stop();
    setState(() {
      _isServiceRunning = false;
    });
  }

  @override
  void initState() {
    super.initState();
    _checkServiceStatus();
  }

  Future<void> _checkServiceStatus() async {
    final isRunning = await geofenceService.isRunning();
    setState(() {
      _isServiceRunning = isRunning;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Geofence Service Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: _isServiceRunning ? null : _startGeofenceService,
              child: const Text('Start Geofence Service'),
            ),
            ElevatedButton(
              onPressed: _isServiceRunning ? _stopGeofenceService : null,
              child: const Text('Stop Geofence Service'),
            ),
            Text('Service is running: $_isServiceRunning'),
          ],
        ),
      ),
    );
  }
}

Step 4: Handling Geofence Transitions

When a geofence transition occurs (i.e., the user enters or exits a geofence), the registered background service will trigger the onGeofenceStatusChanged callback. Handle this callback to perform the desired actions, such as sending a local notification.

geofenceService.addGeofenceStatusStream((geofence, geofenceStatus, location) {
  print('Geofence: $geofence, Status: $geofenceStatus, Location: $location');

  // Handle Geofence Status Changes
  switch (geofenceStatus) {
    case GeofenceStatus.ENTER:
      _showNotification('Entered Geofence', 'You have entered $geofence.id.');
      break;
    case GeofenceStatus.EXIT:
      _showNotification('Exited Geofence', 'You have exited $geofence.id.');
      break;
    case GeofenceStatus.DWELL:
      _showNotification('Dwelling in Geofence', 'You are dwelling in $geofence.id.');
      break;
    default:
      break;
  }
});

Future<void> _showNotification(String title, String body) async {
  // Implementation to show local notifications
  // Refer to documentation for flutter_local_notifications package
}

Example of Displaying a Local Notification

You can use the flutter_local_notifications package to display local notifications when a geofence event occurs. First, add the dependency:

dependencies:
  flutter_local_notifications: ^13.0.0

Then, set up the notification service:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future<void> initializeNotifications() async {
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('app_icon');

  const InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
  );

  await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}

Future<void> _showNotification(String title, String body) async {
  const AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails(
    'geofence_channel',
    'Geofence Notifications',
    channelDescription: 'Notifications triggered by geofence events',
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker',
  );

  const NotificationDetails notificationDetails = NotificationDetails(
    android: androidNotificationDetails,
  );

  await flutterLocalNotificationsPlugin.show(
      0, title, body, notificationDetails);
}

Make sure to call initializeNotifications() when your app starts.

Permissions

Request the necessary permissions for location services in both Android and iOS. In your Flutter app, you can use the permission_handler package to manage permissions.

dependencies:
  permission_handler: ^10.0.0

Request permissions in your app:

import 'package:permission_handler/permission_handler.dart';

Future<void> requestPermissions() async {
  Map<Permission, PermissionStatus> statuses = await [
    Permission.locationAlways,
  ].request();

  print(statuses[Permission.locationAlways]);
}

Troubleshooting

  • Permissions Not Granted: Ensure that location permissions are properly granted in the app settings.
  • Background Service Issues: Verify that the background service is running correctly and has the necessary permissions to access location data in the background.
  • Geofence Not Triggering: Check the geofence settings (latitude, longitude, radius) and make sure they are correctly defined.
  • Location Accuracy: Improve location accuracy by enabling high accuracy mode in the device settings.

Best Practices

  • Battery Optimization: Optimize geofencing to minimize battery drain. Use appropriate geofence radii and avoid unnecessary background location tracking.
  • Error Handling: Implement robust error handling to gracefully handle scenarios where location services are not available or geofencing fails.
  • User Privacy: Clearly communicate to users how their location data is being used and obtain their consent.

Conclusion

Implementing geofencing in Flutter enables you to create location-aware applications that can trigger actions when users enter or exit predefined areas. By following the steps outlined in this guide, you can integrate geofencing functionality into your Flutter projects and enhance user experiences. Always prioritize user privacy and optimize for performance to ensure the best possible user experience.