Flutter Geofencing: Building Location-Aware Apps Without a Backend
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
Building a location-aware app with geofencing in Flutter is a great way to add smart, automated features like automatic check-ins, location-based reminders, or asset tracking. The core idea is simple: define a virtual perimeter (a geofence) around a point of interest, and get notified when the user’s device enters or exits that area. However, implementing this reliably, especially without a backend server, is where the real challenge begins.
The primary hurdle isn’t the initial setup—it’s ensuring your app receives these critical geofence events no matter what. In reality, you’re battling platform-specific behaviors, aggressive battery optimizations, and the dreaded “app kill” by the system. If your geofencing logic stops the moment the user swipes the app away, your feature becomes useless.
Why Off-the-Shelf Packages Can Fall Short
Many developers start with popular geofencing plugins from pub.dev. While they provide a good abstraction, they often rely on the platform’s native geofencing APIs. The issue is that these native services can be terminated by the system to conserve battery. When this happens, events are silently dropped. For a feature like automatic time tracking, missing an “exit” event could mean an employee is incorrectly logged as being at work all night.
A Robust, No-Backend Strategy
To build a resilient solution, you need to create redundant layers of triggers to wake your app up and process location data. Here’s a practical approach:
1. The Foundation: Platform-Specific Background Execution
You must set up proper background execution on both iOS and Android.
For Android (android/app/src/main/AndroidManifest.xml):
You need a foreground service for reliable background work and a broadcast receiver to catch system events like boot completion.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application ...>
<service
android:name=".LocationForegroundService"
android:foregroundServiceType="location"
android:exported="false"/>
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
For iOS (ios/Runner/Info.plist):
Add the required location background modes and a meaningful description.
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs your location to automatically log your work hours when you arrive and leave.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs your location to set up your workplace geofence.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
2. Implementing a Hybrid Location Strategy
Instead of relying solely on native geofencing, combine it with periodic location fetching. Use a plugin like location to fetch location at intervals, and then in your Dart code, manually check if you’re inside your geofence.
Create a core geofence logic class in Dart:
import 'dart:math';
class GeofenceManager {
final double fenceLatitude;
final double fenceLongitude;
final double fenceRadius; // in meters
GeofenceManager({
required this.fenceLatitude,
required this.fenceLongitude,
required this.fenceRadius,
});
bool isWithinFence(double userLat, double userLon) {
const earthRadius = 6371000.0; // meters
double dLat = _toRadians(userLat - fenceLatitude);
double dLon = _toRadians(userLon - fenceLongitude);
double a = sin(dLat / 2) * sin(dLat / 2) +
cos(_toRadians(fenceLatitude)) *
cos(_toRadians(userLat)) *
sin(dLon / 2) *
sin(dLon / 2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
double distance = earthRadius * c;
return distance <= fenceRadius;
}
double _toRadians(double degree) {
return degree * pi / 180;
}
}
// Usage in your background location callback
void locationCallback(LocationDto locationDto) async {
final geofence = GeofenceManager(
fenceLatitude: 37.427961,
fenceLongitude: -122.085749,
fenceRadius: 100.0,
);
bool inside = geofence.isWithinFence(
locationDto.latitude,
locationDto.longitude,
);
// Compare with previous state (stored in shared_preferences)
// and log an entry event or exit event accordingly.
await _processGeofenceTransition(inside);
}
3. Handling Battery Optimizations
On Android, you must guide the user to disable battery optimizations for your app. Use a custom platform channel to check the current setting and open the relevant settings screen.
import 'package:flutter/services.dart';
class BatteryOptimizationHelper {
static const platform = MethodChannel('com.example.app/power');
static Future<bool> isIgnoringBatteryOptimizations() async {
try {
final bool result = await platform.invokeMethod('isIgnoringBatteryOptimizations');
return result;
} on PlatformException catch (e) {
print("Failed to check battery optimizations: '${e.message}'.");
return false;
}
}
static Future<void> openBatteryOptimizationSettings() async {
try {
await platform.invokeMethod('openBatterySettings');
} on PlatformException catch (e) {
print("Failed to open settings: '${e.message}'.");
}
}
}
Storing Data Locally and Safely
Since there’s no backend, all event data must be stored persistently on the device. Use sqflite or hive for robust local storage. Each geofence transition (enter/exit) should be timestamped and saved immediately. Provide a clear UI screen where users can view and export this log.
Key Takeaways and Common Pitfalls
- Don’t Rely on a Single Trigger: Combine native geofencing with periodic background location checks for redundancy.
- Handle the Boot Event: Register a broadcast receiver to restart your location services after a device reboot.
- User Education is Part of the App: Your onboarding must clearly explain why background location and disabling battery optimizations are necessary.
- Test on Real Devices: Emulators are insufficient. Test on a variety of physical Android devices.
By implementing this layered approach, you can build a Flutter geofencing system that stands a much better chance of surviving in the wild—all without a backend. It requires more upfront work and a deep dive into platform channels, but the result is a truly autonomous and reliable location-aware application.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB
Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.
Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls
Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.
Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter
Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.