How Geofencing define the Dart API?
Before signing any platform-specific code, I’ll first want to determine the Dart API for the geofencing plugin. Since Android and ios, several have their own APIs for designating and managing geofencing events, I want the Dart interface to present a reduced geofencing abstraction that is harmonious with both programs. Without going into too much detail about Android and iOS APIs, the following approximately represents the Dart interface that I’ll use for my plugin:
abstract class GeofenceRegion { /// The ID associated with the geofence. /// /// This ID identifies the geofence and is required to delete a /// specific geofence. final String id; /// The location (center point) of the geofence. final Location location; /// The radius around `location` that is part of the geofence. final double radius; /// Listen to these geofence events. final Listtriggers; /// Android-specific settings for a geofence. final AndroidGeofencingSettings androidSettings; GeofenceRegion( this.id, double latitude, double longitude, this.radius, this.triggers, {AndroidGeofencingSettings androidSettings}); } abstract class GeofencingPlugin { /// Initialize the plugin and request relevant permissions from the user. static Future initialize() async; /// Register for geofence events for a [GeofenceRegion]. /// /// `region` is the geofence region to register with the system. /// `callback` is the method to be called when a geofence event associated /// with `region` occurs. static Future registerGeofence( GeofenceRegion region, void Function(List id, Location location, GeofenceEvent event) callback); /// Stop receiving geofence events for a given [GeofenceRegion]. static Future removeGeofence(GeofenceRegion region); /// Stop receiving geofence events for an identifier associated with a /// geofence region. static Future removeGeofenceById(String id) async; }
This interface provides the below programmers to users of the plugin: The ability to create instances of GeofenceRegion, which include the emerges and radius of a geofence, a unique ID, and a collection of geofencing programmers to listen for. Since Android gives a richer set of lists for explaining geofences than iOS, Android-specific opportunities are made getting through the chosen androidSettings property. GeofencingPlugin.registerGeofence makes for the registration of a GeofenceRegion instance with a callback that is involved when a geofence event for that area is got. GeofencingPlugin.removeGeofence and GeofencingPlugin.removeGeofenceById will not a GeofenceRegion from triggering more events. Overall, this mode is rather simple and action-agnostic, making the plugin simple to act on both Android and iOS. Dart surronding execution This segment covers how to set up your isolate for experience execution. You will discover how to reference callbacks, and how to use the callback dispatcher. Referencing Callbacks Now that the Dart interface is defined, start figuring plumbing to communicate with the platform-particular divisions of the plugin. For example, the following program initializes the geofencing plugin and displays the geofences:
abstract class GeofencingPlugin { static const MethodChannel _channel = const MethodChannel('plugins.flutter.io/geofencing_plugin'); static Futureinitialize() async { final callback = PluginUtilities.getCallbackHandle(callbackDispatcher); await _channel.invokeMethod('GeofencingPlugin.initializeService', [callback.toRawHandle()]); } static Future registerGeofence( GeofenceRegion region, void Function(List id, Location location, GeofenceEvent event) callback) { if (Platform.isIOS && region.triggers.contains(GeofenceEvent.dwell) && (region.triggers.length == 1)) { throw UnsupportedError("iOS does not support 'GeofenceEvent.dwell'"); } final args = [ PluginUtilities.getCallbackHandle(callback).toRawHandle() ]; args.addAll(region._toArgs()); _channel.invokeMethod('GeofencingPlugin.registerGeofence', args); } /* * … `removeGeofence` methods here … */ }
If you’ve previously created Flutter plugins and are common with MethodChannel, this should look is wanted, for the most part. However, the two calls to PluginUtilities.getCallbackHandle might outstanding. In order to provoke a Dart callback as a result of a surrounding event, you must take a handle that is passed among Dart and platform code while also keeping for lookup of the callback across platform threads and Dart. Retrieving a CallbackHandle for a method from PluginUtilities.getCallbackHandle has the bad effect of multiplying a callback cache within the Flutter engine. These cache maps details needed to wanted callbacks to raw integer handles, which are easy hashes added aimed at the properties of the callback. This cache persists across publishes, but be know that callback lookups may lose if the callback is renamed or moved and PluginUtilities.getCallbackHandle is not called for the updated version of callback. In the code that given, two instances of CallbackHandle are accessed one for the callback, which is assisted with a GeofenceRegion, and another for an option of the name callbackDispatcher. The callbackDispatcher method, the entry point of the background loneliness, is the reason for preprocessing raw geofence event data, seeing up callbacks via PluginUtilities.getCallbackFromHandle, and invoking them for registered geofences. The Callback Dispatcher As told at the end of the previous section, This pattern access for acting the initialization needed to establish connection channels with acting code while also allowing for the formation of non-trivial interfaces for callback options. For this geofencing plugin, the callback dispatcher planning is as follows:
void callbackDispatcher() { const MethodChannel _backgroundChannel = MethodChannel('plugins.flutter.io/geofencing_plugin_background'); WidgetsFlutterBinding.ensureInitialized(); _backgroundChannel.setMethodCallHandler((MethodCall call) async { final args = call.arguments; final Function callback = PluginUtilities.getCallbackFromHandle( CallbackHandle.fromRawHandle(args[0])); assert(callback != null); // 3.2. Preprocess arguments. final triggeringGeofences = args[1].cast(); final locationList = args[2].cast (); final triggeringLocation = locationFromList(locationList); final GeofenceEvent event = intToGeofenceEvent(args[3]); callback(triggeringGeofences, triggeringLocation, event); }); _backgroundChannel.invokeMethod('GeofencingService.initialized'); }
As you can see, on the formation of callbackDispatcher only four methods are performed. a MethodChannel is made for listening to options from the plugin. Next, WidgetsFlutterBinding.ensureInitialized() is known to initialize the state required to connect with the Flutter engine. At this point, the MethodCall handler is set to work with plugin programmers before at last notifying the action portion of the plugin that the background makes it is mentioned and starting the handling events.
Once the plugin is created transform programmers to the callback dispatcher, the callback given to the plugin user can be invoked. First, PluginUtilities.getCallbackFromHandle is known to take a moment of the callback assisted with the provoked geofencing program using the raw callback to hold. Next, the raw materials from the MethodCall are made into:
A moment of List
Background execution: Android For the Android installation of the plugin, I’ll be required to make the following sections: The GeofencingPlugin class, which is created with the Flutter engine in order to get and handle method calls built from Dart code A GeofencingBroadcastReceiver, which is provoked by the system on a geofence event The GeofencingService, which made the background isolate, merges the callback dispatcher defined earlier and processes geofence programmes invoking the callback dispatcher. Creating Geofences In order to influence options, select an instance of MethodChannel on the same channel from the past, and then register the GeofencingPlugin instance with this channel in the implementation of onAttachedToEngine:
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { mContext = binding.getApplicationContext() mGeofencingClient = LocationServices.getGeofencingClient(mContext!!) val channel = MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/geofencing_plugin") channel.setMethodCallHandler(this) }
In order to manage these needs, onMethodCall is required to install:
override fun onMethodCall(call: MethodCall, result: Result) { val args = call.arguments>() when(call.method) { "GeofencingPlugin.initializeService" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mActivity?.requestPermissions(REQUIRED_PERMISSIONS, 12312) } // Simply stores the callback handle for the callback dispatcher initializeService(mContext!!, args) result.success(true) } "GeofencingPlugin.registerGeofence" -> registerGeofence(mContext!!, mGeofencingClient!!, args, result, true) "GeofencingPlugin.removeGeofence" -> removeGeofence(mContext!!, mGeofencingClient!!, args, result) else -> result.notImplemented() } }
At last, include the skill to register geofences
@JvmStatic private fun getGeofencingRequest(geofence: Geofence, initialTrigger: Int): GeofencingRequest { return GeofencingRequest.Builder().apply { setInitialTrigger(initialTrigger) addGeofence(geofence) }.build() } @JvmStatic private fun getGeofencePendingIndent(context: Context, callbackHandle: Long): PendingIntent { val intent = Intent(context, GeofencingBroadcastReceiver::class.java) .putExtra(CALLBACK_HANDLE_KEY, callbackHandle) return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } // TODO(bkonyi): Reregister geofences after reboot // https://developer.android.com/training/location/geofencing @JvmStatic private fun registerGeofence(context: Context, geofencingClient: GeofencingClient, args: ArrayList<*>?, result: Result?) { val callbackHandle = args!![0] as Long val id = args[1] as String val lat = args[2] as Double val long = args[3] as Double val radius = (args[4] as Number).toFloat() val fenceTriggers = args[5] as Int val initialTriggers = args[6] as Int val expirationDuration = (args[7] as Int).toLong() val loiteringDelay = args[8] as Int val notificationResponsiveness = args[9] as Int val geofence = Geofence.Builder() .setRequestId(id) .setCircularRegion(lat, long, radius) .setTransitionTypes(fenceTriggers) .setLoiteringDelay(loiteringDelay) .setNotificationResponsiveness(notificationResponsiveness) .setExpirationDuration(expirationDuration) .build() // Ensure permissions are set properly. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)) { val msg = "'registerGeofence' requires the ACCESS_FINE_LOCATION permission." Log.w(TAG, msg) result?.error(msg, null, null) } ofencingClient.addGeofences(getGeofencingRequest(geofence, initialTriggers), getGeofencePendingIndent(context, callbackHandle))?.run { addOnSuccessListener { Log.i(TAG, "Successfully added geofence") result?.success(true) } addOnFailureListener { Log.e(TAG, "Failed to add geofence: $it") result?.error(it.toString(), null, null) } } }
Scheduling the geofencing service:
class GeofencingBroadcastReceiver : BroadcastReceiver() { companion object { private const val TAG = "GeofencingBroadcastReceiver" } override fun onReceive(context: Context, intent: Intent) { FlutterMain.startInitialization(context) FlutterMain.ensureInitializationComplete(context, null) GeofencingService.enqueueWork(context, intent) } }
Handling Geofence Events using the below code:
private fun startGeofencingService(context: Context) { synchronized(sServiceStarted) { mContext = context // dispatcher. if (sBackgroundFlutterEngine == null) { val callbackHandle = context.getSharedPreferences( GeofencingPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) .getLong(GeofencingPlugin.CALLBACK_DISPATCHER_HANDLE_KEY, 0) if (callbackHandle == 0L) { Log.e(TAG, "Fatal: no callback registered") return } val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) if (callbackInfo == null) { Log.e(TAG, "Fatal: failed to find callback") return } sBackgroundFlutterEngine = FlutterEngine(context) val args = DartCallback( context.getAssets(), FlutterMain.findAppBundlePath(context)!!, callbackInfo ) sBackgroundFlutterEngine!!.getDartExecutor().executeDartCallback(args) } } mBackgroundChannel = MethodChannel(sBackgroundFlutterEngine!!.getDartExecutor().getBinaryMessenger(), "plugins.flutter.io/geofencing_plugin_background") mBackgroundChannel.setMethodCallHandler(this) }
After startGeofencingService is completed execution, onHandleWork is called by the machine with the Intent that was lined up earlier:
val locationList = listOf(location.latitu override fun onHandleWork(intent: Intent) { val callbackHandle = intent. getLongExtra(GeofencingPlugin.CALLBACK_HANDLE_KEY, 0) val geofencingEvent = GeofencingEvent.fromIntent(intent) if (geofencingEvent.hasError()) { Log.e(TAG, "Geofencing error: ${geofencingEvent.errorCode}") return } val geofenceTransition = geofencingEvent.geofenceTransition val triggeringGeofences = geofencingEvent.triggeringGeofences.map { it.requestId } val location = geofencingEvent.triggeringLocation de, location.longitude) val geofenceUpdateList = listOf(callbackHandle, triggeringGeofences, locationList, geofenceTransition) synchronized(sServiceStarted) { if (!sServiceStarted.get()) { queue.add(geofenceUpdateList) } else { mBackgroundChannel.invokeMethod("", geofenceUpdateList) } } }
You have to write the below code next:
override fun onMethodCall(call: MethodCall, result: Result) { if (call.method == "GeofencingService.initialized") { synchronized(sServiceStarted) { while (!queue.isEmpty()) { mBackgroundChannel.invokeMethod("", queue.remove()) } sServiceStarted.set(true) result.success(null) } } else { result.notImplemented() } }
At this point, the GeofencingService is fully initialized and any geofencing events that have lined up are sent to the callback dispatcher. Background execution: iOS Now that the geofencing plugin implementation for Android is completed, the same geofencing program is required to be implemented for iOS. Initializing the plugin using the below code
+ (void)registerWithRegistrar:(NSObject*)registrar { @synchronized(self) { if (instance == nil) { NSLog(@"Registering with registrar"); instance = [[GeofencingPlugin alloc] init:registrar]; [registrar addApplicationDelegate:instance]; } } }
Additional state for the plugin is created when the GeofencingPlugin instance is build during plugin registration:
- (instancetype)init:(NSObject*)registrar { self = [super init]; NSAssert(self, @"super init cannot be nil"); _persistentState = [NSUserDefaults standardUserDefaults]; _locationManager = [[CLLocationManager alloc] init]; [_locationManager setDelegate:self]; [_locationManager requestAlwaysAuthorization]; _locationManager.accessBackgroundLocationUpdates = YES; _headlessRunner = [[FlutterEngine alloc] initWithName:@"GeofencingIsolate" project:nil allowHeadlessExecution:YES]; _registrar = registrar; _mainChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/geofencing_plugin" binaryMessenger:[registrar messenger]]; [registrar addMethodCallDelegate:self channel:_mainChannel]; _callbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/geofencing_plugin_background" binaryMessenger:_headlessRunner]; return self; }
Starting the callback dispatcher:
- (void)startGeofencingService:(int64_t)handle { NSLog(@"Initializing GeofencingService"); [self setCallbackDispatcherHandle:handle]; FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:handle]; NSAssert(info != nil, @"failed to find callback"); NSString *entrypoint = info.callbackName; NSString *uri = info.callbackLibraryPath; [_headlessRunner runWithEntrypointAndLibraryUri:entrypoint libraryUri:uri]; NSAssert(registerPlugins != nil, @"failed to set registerPlugins"); registerPlugins(_headlessRunner); [_registrar addMethodCallDelegate:self channel:_callbackChannel]; }
Handling method calls:
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { NSArray *arguments = call.arguments; if ([@"GeofencingPlugin.initializeService" isEqualToString:call.method]) { NSAssert(arguments.count == 1, @"Invalid argument count for 'GeofencingPlugin.initializeService'"); [self startGeofencingService:[arguments[0] longValue]]; result(@(YES)); } else if ([@"GeofencingService.initialized" isEqualToString:call.method]) { // Ignored on iOS. result(nil); } else if ([@"GeofencingPlugin.registerGeofence" isEqualToString:call.method]) { [self registerGeofence:arguments]; result(@(YES)); } else if ([@"GeofencingPlugin.removeGeofence" isEqualToString:call.method]) { result(@([self removeGeofence:arguments])); } else { result(FlutterMethodNotImplemented); } }
Registering geofences:
- (void)registerGeofence:(NSArray *)arguments { NSLog(@"RegisterGeofence: %@", arguments); int64_t callbackHandle = [arguments[0] longLongValue]; NSString *identifier = arguments[1]; double latitude = [arguments[2] doubleValue]; double longitude = [arguments[3] doubleValue]; double radius = [arguments[4] doubleValue]; CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:CLLocationCoordinate2DMake(latitude, longitude) radius:radius identifier:identifier]; region.notifyOnEntry = YES; region.notifyOnExit = YES; [self setCallbackHandleForRegionId:callbackHandle regionId:identifier]; [self->_locationManager startMonitoringForRegion:region]; }
For Handling geofence events use the below code:
- (void)sendLocationEvent:(CLRegion *)region eventType:(int)event { NSAssert([region isKindOfClass:[CLCircularRegion class]], @"region must be CLCircularRegion"); CLLocationCoordinate2D center = region.center; int64_t handle = [self getCallbackHandleForRegionId:region.identifier]; [_callbackChannel invokeMethod:@"" arguments:@[ @(handle), @[ region.identifier ], @[ @(center.latitude), @(center.longitude) ], @(event) ] ]; } - (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region { [self sendLocationEvent:region eventType:kEnterEvent]; } - (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region { [self sendLocationEvent:region eventType:kExitEvent]; }
For Geofence events in a suspended state below code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Check to see if we're being launched due to a location event. if (launchOptions[UIApplicationLaunchOptionsLocationKey] != nil) { // Restart the headless service. [self startGeofencingService:[self getCallbackDispatcherHandle]]; } return YES; }
To get Permissions: Android use the below code:
To get Permissions in iOS:
… UIRequiredDeviceCapabilities location-services gps armv7 UIBackgroundModes … location … NSLocationAlwaysAndWhenInUseUsageDescription YOUR DESCRIPTION HERE NSLocationWhenInUseUsageDescription YOUR DESCRIPTION HERE …
These descriptions are shown to the person who uses them when the app requests access to themarea.
#include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" // Add the import for the GeofencingPlugin. #importvoid registerPlugins(NSObject * registry) { [GeneratedPluginRegistrant registerWithRegistry:registry]; } @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Register the plugins with the AppDelegate registerPlugins(self); // Set registerPlugins as a callback within GeofencingPlugin. This access // for the Geofencing plugin to register the plugins with the background // FlutterEngine instance created to handle events. If this step is skipped, // other plugins will not work in the geofencing callbacks! [GeofencingPlugin setPluginRegistrantCallback:registerPlugins]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end
Bringing it all together:
Futureinitialize() async { // Perform other initialization // … // Initialize the geofencing plugin. await GeofencingManager.initialize(); } Widget _proximityTriggerToggle() => Container( padding: const EdgeInsets.fromLTRB(26.0, 2.5, 26.0, 2.5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Proximity Trigger', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), Switch( value: _proximityTriggerEnabled, onChanged: (bool state) async { setState(() { _proximityTriggerEnabled = state; }); if (state) { await GeofencingManager.registerGeofence( GeofenceTrigger.homeRegion, GeofenceTrigger.homeGeofenceCallback); } else { await GeofencingManager.removeGeofence( GeofenceTrigger.homeRegion); } }, ), ], ) ); abstract class GeofenceTrigger { static final _androidSettings = AndroidGeofencingSettings( initialTrigger: [GeofenceEvent.exit], notificationResponsiveness: 0, loiteringDelay: 0); static bool _isInitialized = false; static final homeRegion = GeofenceRegion( 'home', HOME_LAT, HOME_LONG, 300.0, [GeofenceEvent.enter], androidSettings: _androidSettings); static Future homeGeofenceCallback( List id, Location location, GeofenceEvent event) async { // Check to see if this is the first time the callback is being called. if (!_isInitialized) { // Re-initialize state required to communicate with the garage door // server. await initialize(); _isInitialized = true; } if (event == GeofenceEvent.enter) { await GarageDoorRemote.openDoor(); } } }
At last, you execute a dart in the background with flutter plugin and geofencing
If you have any questions about the above topic or have to get services and consultations to setup flutter and dart plugins. Feel free to contact us. AIRO GLOBAL SOFTWARE will be your digital solution. E-mail id: [email protected]
Author - Johnson Augustine
Chief Technical Director and Programmer
ounder: Airo Global Software Inc
LinkedIn Profile: www.linkedin.com/in/johnsontaugustine/