Implementing Geofencing in Flutter

Geofencing is a powerful feature in mobile app development that allows you to define virtual boundaries (geofences) and trigger actions when a device enters or exits these boundaries. In Flutter, you can implement geofencing to create location-aware applications that respond to user movements. This comprehensive guide will walk you through the process of implementing geofencing in Flutter, complete with code examples.

What is Geofencing?

Geofencing is the creation of a virtual perimeter around a real-world geographic area. By setting up a geofence, your app can receive notifications when a device crosses its boundary—either entering (dwell transition) or exiting (exit transition). This technology is used in various applications, such as:

  • Location-based reminders
  • Proximity marketing
  • Security alerts
  • Automated check-ins

Why Use Geofencing in Flutter?

  • Engage Users: Deliver location-relevant content or promotions.
  • Improve User Experience: Provide contextual interactions based on their physical location.
  • Increase App Utility: Automate tasks and provide timely notifications.

Implementing Geofencing in Flutter: A Step-by-Step Guide

To implement geofencing in Flutter, you will need to use plugins that provide access to native platform APIs for geofencing. We’ll be using the geofence plugin for this example.

Step 1: Set Up a New Flutter Project

First, create a new Flutter project if you don’t have one already:

flutter create geofence_app

Step 2: Add Dependencies

Add the geofence plugin to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  geofence: ^0.2.1+2  # Use the latest version

Run flutter pub get to install the dependencies.

Step 3: Configure AndroidManifest.xml (Android)

Add the necessary permissions and service declarations in your android/app/src/main/AndroidManifest.xml file:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="your.package.name">

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <application
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher"
        android:label="geofence_app">

        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:exported="true"
            android:hardwareAccelerated="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:windowSoftInputMode="adjustResize">
            <meta-data
                android:name="io.flutter.embedding.android.SplashScreenDrawable"
                android:resource="@drawable/launch_background"/>

            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2"/>

        <receiver
            android:name="com.transistorsoft.flutter.backgroundfetch.BackgroundFetch$BootReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

        <service
            android:name="com.transistorsoft.flutter.backgroundfetch.BackgroundFetch$HeadlessTaskService"
            android:exported="false"
            android:permission="android.permission.BIND_JOB_SERVICE">
            <intent-filter>
                <action android:name="android.intent.action.HEADLESS_JS_TASK"/>
            </intent-filter>
        </service>
    </application>
</manifest>

Ensure to request location permissions at runtime. Add the following lines inside the <application> tag:

    <service
        android:name="com.pravera.geofence.GeofenceService"
        android:enabled="true"
        android:exported="false"/>

    <receiver
        android:name="com.pravera.geofence.GeofenceBroadcastReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
            <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
        </intent-filter>
    </receiver>

Step 4: Configure Info.plist (iOS)

Add the necessary permissions in your ios/Runner/Info.plist file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>This app needs location access to work.</string>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>This app needs location access to work.</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>This app needs location access to work.</string>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>geofence_app</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>$(FLUTTER_BUILD_NAME)</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>$(FLUTTER_BUILD_NUMBER)</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UIViewControllerBasedStatusBarAppearance</key>
    <false/>
</dict>
</plist>

Add the following keys with descriptions:

  • NSLocationWhenInUseUsageDescription
  • NSLocationAlwaysUsageDescription
  • NSLocationAlwaysAndWhenInUseUsageDescription

Step 5: Request Permissions

Before using geofencing, request location permissions from the user. Use the permission_handler plugin for this:

dependencies:
  permission_handler: ^11.3.0

Request the necessary permissions in your Dart code:

import 'package:permission_handler/permission_handler.dart';

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

  print(statuses[Permission.locationAlways]);
  print(statuses[Permission.locationWhenInUse]);
}

Call this function in your app’s initState or before using geofencing functionalities.

Step 6: Implement Geofencing Logic

Create and manage geofences in your Flutter application.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:geofence/geofence.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _activity = 'Unknown';
  String _latitude = '';
  String _longitude = '';
  String _address = '';

  @override
  void initState() {
    super.initState();
    requestPermissions();
    initGeofence();
  }

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

    print(statuses[Permission.locationAlways]);
    print(statuses[Permission.locationWhenInUse]);
  }

  Future<void> initGeofence() async {
    Geofence.initialize();

    Geofence.getActivityStream()?.listen((Activity result) {
      setState(() {
        _activity = result.toString();
      });
    });

    Geofence.getGeolocationStream()?.listen((Geolocation result) {
      setState(() {
        _latitude = result.latitude.toString();
        _longitude = result.longitude.toString();
      });
    });

    Geofence.getAddressStream()?.listen((Address result) {
      setState(() {
        _address = '${result.countryName}, ${result.adminArea}, ${result.locality}';
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Geofencing in Flutter'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Activity: $_activityn'),
              Text('Latitude: $_latituden'),
              Text('Longitude: $_longituden'),
              Text('Address: $_addressn'),
              ElevatedButton(
                onPressed: () {
                  createGeofence();
                },
                child: Text('Create Geofence'),
              ),
              ElevatedButton(
                onPressed: () {
                  removeGeofence();
                },
                child: Text('Remove Geofence'),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> createGeofence() async {
    try {
      await Geofence.addGeolocation(
        GeofenceRegion(
          id: 'myGeofence',
          latitude: 37.7749, // Example Latitude
          longitude: -122.4194, // Example Longitude
          radius: [
            RadiusDescriptor(id: '100m', length: 100),
          ],
          // Allows transitionType to be a String or a TransitionType Enum.
          transitionType: TransitionType.EXIT,
        ),
      );
      print('Geofence created successfully');
    } catch (e) {
      print('Error creating geofence: $e');
    }
  }

  Future<void> removeGeofence() async {
    try {
      await Geofence.removeGeolocation('myGeofence');
      print('Geofence removed successfully');
    } catch (e) {
      print('Error removing geofence: $e');
    }
  }
}

In this example:

  • The initState method initializes geofencing and requests location permissions.
  • The createGeofence method adds a geofence with specific coordinates and a radius.
  • The removeGeofence method removes the created geofence.
  • The UI displays the current activity, location, and address, along with buttons to create and remove geofences.

Testing Your Geofencing Implementation

Testing geofencing requires simulating location changes. You can use tools such as the Android emulator or Xcode’s simulator to mock locations and verify that your app responds correctly to geofence transitions.

Best Practices for Geofencing

  • Handle Permissions Gracefully: Always check and request necessary permissions.
  • Optimize Battery Usage: Use appropriate geofence radii and transition types to minimize battery drain.
  • Handle Errors: Implement proper error handling to manage scenarios where location services are unavailable.
  • Inform Users: Clearly explain why your app needs location permissions.

Conclusion

Implementing geofencing in Flutter can significantly enhance your app’s functionality by making it location-aware. By following this comprehensive guide, you can create applications that respond intelligently to user movements within specified geographic boundaries. Be sure to handle permissions, optimize battery usage, and thoroughly test your implementation to provide the best possible user experience.