Executing Dart in the Background with Flutter Plugins and Geofencing

Ben Konyi
Ben Konyi
Sep 20, 2018 · 16 min read
No garage door remote? Not a problem with Flutter and a Raspberry Pi!

09/10/2020: This article has been updated to reflect the move to the v2 Android embedding API. Migration instructions for applications created pre-1.12 can be found here, and here for plugins.

Whether handling push notifications, location updates, or sensor events, many useful features require that an application has the ability to handle events without user interaction, even when not running in the foreground. Up until this point, applications written using Flutter could only handle background events using platform code, and plugins had no way of allowing for plugin users to provide a callback to handle background events in Dart. Basically, if a Flutter user wants to handle background events in an application, they were required to create a platform-specific implementation for each of their target platforms.

Luckily, this is no longer the situation, thanks to the arrival of Flutter support for background execution of Dart code. Having designed much of the Flutter background execution flow, I’m excited to share my experiences developing plugins which take advantage of this functionality, such as the android_alarm_manager and an iOS location event handler, to help you get started creating your own plugins.

Throughout the rest of this article, I’ll explore (in detail) the process of building a Flutter plugin that handles background geofencing events on both Android and iOS. Finally, just for fun, I’ll showcase how I used this geofencing plugin to create a simple application that can be used to open my garage door automatically when I get close to home.

Table of Contents

Geofencing: defining the Dart API

This interface provides the following functionality to users of the plugin:

  • The ability to create instances of GeofenceRegion, which contain the coordinates and radius of a geofence, a unique ID, and a list of geofencing events to listen for. Since Android provides a richer set of options for defining geofences than iOS, Android-specific options are made available through the optional androidSettings property.
  • GeofencingPlugin.registerGeofence allows for the registration of a GeofenceRegion instance with a callback that is invoked when a geofence event for that region is received.
  • GeofencingPlugin.removeGeofence and GeofencingPlugin.removeGeofenceById unregister a GeofenceRegion from triggering additional events.

Overall, this interface is rather simple and (mostly) platform agnostic, making the plugin easy to use on both Android and iOS.

Dart background execution

Referencing Callbacks

If you’ve previously developed Flutter plugins and are familiar with MethodChannel, this should look as expected, for the most part. (If you’re new to plugin development, check out the platform channels article for an introduction). However, the two calls to PluginUtilities.getCallbackHandle might stand out.

Callback handles are managed by the Flutter engine and can be used to reference and lookup callbacks across isolates.

In order to invoke a Dart callback as a result of a background event, you must retrieve a handle that is passed between Dart and platform code while also allowing for lookup of the callback across platform threads and Dart isolates.

Aside: Retrieving a CallbackHandle for a method from PluginUtilities.getCallbackHandle has the side effect of populating a callback cache within the Flutter engine, as seen in the diagram above. This cache maps information required to retrieve callbacks to raw integer handles, which are simply hashes calculated based on the properties of the callback. This cache persists across launches, but be aware that callback lookups may fail if the callback is renamed or moved and PluginUtilities.getCallbackHandle is not called for the updated callback.

In the code above, two instances of CallbackHandle are obtained: one for the callback, which is associated with a GeofenceRegion, and another for a method of the name callbackDispatcher. The callbackDispatcher method, the entrypoint of the background isolate, is responsible for preprocessing raw geofence event data, looking up callbacks via PluginUtilities.getCallbackFromHandle, and invoking them for registered geofences.

The Callback Dispatcher

As you can see, on the invocation of callbackDispatcher (upon the creation of the geofencing plugin’s background isolate), only four operations are performed. First, a MethodChannel is created for listening to events from the plugin. Next, WidgetsFlutterBinding.ensureInitialized() is called to initialize state needed to communicate with the Flutter engine. At this point, the MethodCall handler is set to process plugin events before finally alerting the platform portion of the plugin that the background isolate is initialized and ready to start handling events.

Once the plugin starts sending events to the callback dispatcher, the callback provided by the plugin user can be invoked. First, PluginUtilities.getCallbackFromHandle is called to retrieve an instance of the callback associated with the triggered geofencing event using the raw callback handle. Next, the raw arguments from the MethodCall are refined into:

  • An instance of List<String> for the IDs of the geofences that were triggered
  • An instance of Location describing the current location of the device
  • An instance of the GeofenceEvent enum that represents whether the device has entered, exited, or dwelled within the triggered geofences.

Then provide this info as arguments to our callback.

Important Note: You may have noticed that no state is kept within the callback handler. This is because there is no guarantee that the background isolate will stay alive while the application itself is backgrounded. Both Android and iOS have lifecycle policies that can result in background services or execution being killed, meaning that the background isolate may be destroyed and then recreated the next time the application is woken up. As a result, best practice avoids storing volatile state in either the callback handler or user-provided callbacks.

At this point, we now have all of the Dart code needed for the plugin! Now, onto the platform-specific portion of the geofencing plugin.

Background execution: Android (Kotlin)

  • The GeofencingPlugin class, which is registered with the Flutter engine in order to receive and handle method calls made from Dart code
  • A GeofencingBroadcastReceiver, which is invoked by the system on a geofence event
  • The GeofencingService, which creates the background isolate, initializes the callback dispatcher described earlier, and processes geofence events before invoking the callback dispatcher.

This trinity of 1) plugin, 2) broadcast receiver, and 3) service classes is a common pattern for plugins on Android, so it is worth becoming familiar with it. Although I’ve decided to use Kotlin for this plugin, everything here can also be implemented using Java.

GeofencingPlugin

This class implements two interfaces that are required for basic functionality of the plugin:

  • FlutterPlugin: declares onAttachedToEngine and onDetachedFromEngine, used to notify the plugin of its connection status to a Flutter engine instance.
  • MethodCallHandler: declares onMethodCall, the method used to process messages sent to the plugin over a MethodChannel.

Most plugins need to implement both FlutterPlugin and MethodCallHandler, but some plugins may also require information about the current Activity or other application component.

To get access to application components currently attached to the plugin instance, a plugin should implement one or more of the “awareness” interfaces for Activity, BroadcastReceiver, ContentProvider, or Service. These interfaces declare callbacks that can be invoked by the Flutter engine to notify the plugin when a component is attached or detached. For example, a plugin that requires access to an Activity would implement the ActivityAware interface and would be notified when the plugin gains or loses access to an Activity due to the application being minimized.

Creating Geofences

In order to manage these requests, onMethodCall needs to be implemented:

Finally, I’ll add the ability to register geofences (removing geofences is relatively trivial, so I’ll focus on adding geofences in this article):

There’s a lot going on here, so let’s break this down:

  1. Pull the relevant arguments out of the ArrayList sent over the MethodChannel
  2. Create an instance of Geofence that describes the location and size of the geofence as well as its various trigger parameters
  3. Before registering the Geofence instance, do another check to ensure that the application still has the correct device permissions for geofencing
  4. Finally, a GeofencingRequest as well as a PendingIntent are created and used to register the geofence. The PendingIntent is used to invoke the GeofencingBroadcastReceiver when the geofence is triggered; it contains the callback handle associated with that geofence.

That’s it! At this point the plugin can create and register a geofence. However, the plugin is not yet ready to handle actual geofence events. For that, the plugin needs to be able to be woken up by the system when there is a geofence event to be handled.

Scheduling the geofencing service

The onReceive implementation is simple: it ensures that the Flutter framework is initialized and then adds the Intent for the geofencing event to the GeofencingService’s work queue. Since GeofencingService is an implementation of a JobIntentService, GeofencingService.enqueueWork is simply a wrapper around the enqueueWork method in JobIntentService, which handles scheduling the work for the service.

Handling Geofence Events

startGeofencingService is responsible for ensuring that the plugin has an associated FlutterEngine instance. Each FlutterEngine instance provides access to a DartExecutor which can be used to execute Dart code in a new isolate. In this case, the FlutterEngine instance has the important task of initializing the callback dispatcher, and executing the callbacks registered with the plugin.

After startGeofencingService is done executing, onHandleWork is called by the system with the Intent that was queued up earlier:

Most of the above code builds the argument list which is sent to the callback dispatcher. However, before passing the processed geofence event arguments to the callback dispatcher, the plugin must ensure that the callback dispatcher has started listening on its MethodChannel. To achieve this behavior, the GeofencingService listens for a message from the callback dispatcher, which is sent after the MethodCall handler for the dispatcher is set:

At this point, the GeofencingService is completely initialized and any geofencing events that have queued up are sent to the callback dispatcher.

Background execution: iOS (Objective-C)

Initializing the plugin

Additional state for the plugin is set when the GeofencingPlugin instance is created during plugin registration:

Starting the callback dispatcher

Note: the FlutterMethodChannel for the callback dispatcher is only registered after the headless runner has been started. If an attempt to register the callback dispatcher’s method channel is made before this is done, the application will likely crash.

Handling method calls

Registering geofences

This method creates the geofence region and uses the CLLocationManager’s startMonitoringForRegion method to register the geofence. In order to keep track of which callback is associated with the newly registered geofence, the callback handle is mapped to the region’s user provided identifier which is stored to disk using NSUserDefaults. Doing this allows for the plugin to lookup the callback handle when a geofence event is received, even if the application had been closed since the geofence was registered.

Handling geofence events

Geofence events in a suspended state

For geofencing, the plugin needs to implement didFinishLaunchingWithOptions which is invoked when the application has just been started and is ready to run. The dictionary parameter of this method will contain UIApplicationLaunchOptionsLocationKey if the application was launched due to a geofence event.

If the application is launched as the result of a geofence being triggered, the callback dispatcher for the plugin will still need to be initialized by calling startGeofencingService with the cached callback dispatcher handle. After returning from this method, the location manager will invoke the appropriate handler described in the previous section for the geofence event.

Usage example: operating a garage door with geofencing

The relay is triggered by the Raspberry Pi, which opens and closes the door (left). A proximity sensor allows for the garage door service to know whether or not the door is currently open (right).
My garage door remote, built using Flutter. Can you tell that I’m a backend engineer?

Over the past couple of months I’ve been tinkering with a Raspberry Pi and the Dart rpi_gpio package to get back into working with circuits. I had been toying around with the idea of making a Flutter application that would open my garage door for quite awhile, so it made sense for my first hardware project to use Dart and Flutter. Over a couple of weekends, I wrote a service to control the garage door, performed minor surgery to wire a relay across the opener button, installed a proximity sensor to query the state of the door, and finally wrote a simple application using Flutter that functions as a remote control.

After using my solution for a couple of weeks, I was becoming annoyed. Although it was great that I could open the garage with a mobile application, I still had to pull out my phone, open the app, and then manually trigger the door. This is particularly painful for me since fingerprint readers apparently don’t like to work when covered in sweat, making it difficult to unlock my phone (I don’t have a car and I commute 15km each way by bike, so fingerprint reader struggles are a daily occurrence).

Luckily for me, Flutter now has a geofencing plugin that can perform tasks even while the application isn’t open, including opening my garage door!

Setting the right permissions

Permissions: Android

Geofencing on Android also requires the ACCESS_FINE_LOCATION and ACCESS_BACKGROUND_LOCATION (Android 10+) permissions to be requested in AndroidManifest.xml:

Permissions: iOS

Then set the NSLocation description messages:

These descriptions are shown to the user when the application requests access to their location. If they’re not provided, geofencing registration will fail silently!

Finally, similarly to what was done for Android, set a reference to the application’s plugin registrant within GeofencingPlugin from the application’s AppDelegate. This is needed to register the application’s plugins with the geofencing plugin’s background isolate, which makes it possible to use other plugins in the context of that isolate.

Bringing it all together

First of all, the plugin needs to be initialized. This is done in a method named initialize, which is invoked when the application starts:

Next, there should be some way to toggle whether or not the garage door should open when the geofence is entered. This can be accomplished with a simple Switch displayed at the bottom of the application:

Depending on the state of this Switch, the application either registers a geofence with the callback used to open the garage door, or removes the geofence.

Finally, the geofence region around my home and its corresponding callback are defined in GeofenceTrigger:

For my initial tests I created a GeofenceRegion with a radius of 300m around my home that will trigger at some point after I enter the area. Once the geofence is entered the homeGeofenceCallback is invoked, checks are performed to ensure the application can communicate with the garage door server, and then a request to open the door is sent. Once I confirmed that the logic within the callback actually triggered the garage door to open using a third-party application to mock my location and movements, it was time to do some real world testing on my bike!

After a few trips up and down the street it became apparent that, although geofences are triggered almost immediately when using a mocked location, Android provides no guarantees as to when a geofence event is delivered. Unfortunately, this means that it can potentially take minutes before my garage door remote is notified that I’ve entered the geofence region around my home. With a radius as small as 300m, I often found myself waiting a minute or two for the door to open on its own.

The temporary fix for this was to increase the radius of the geofence region to 1km, which seems to work well enough for now. Obviously, there’s some issues with this approach, but I plan on further refining the proximity triggering logic to use a larger geofence that will start more frequent location updates. These location updates will then be used to manually determine whether I’m within a certain radius of my house, at which point the request to open the door will be sent.

Conclusion

I’ve had a lot of fun implementing background execution support for Flutter, and even more fun creating the geofencing plugin for this article. (It was a wonderful excuse to work on a personal project as part of my job!) If you feel so inclined, follow me on GitHub to keep up with my work on Flutter and the Dart virtual machine, as well as my other pet projects.

Thanks for reading and happy Fluttering!

Like many members of the Dart and Flutter teams, Dash loves cycling. However, for obvious reasons, Dash has a bit of trouble riding a bike.

Resources

Docs for Dart:

Docs for Android:

Docs for iOS:

Sample Plugins:

Projects referenced:

Flutter

Flutter is Google's mobile UI framework for crafting…

Flutter

Flutter is Google's mobile UI framework for crafting high-quality native interfaces on iOS and Android in record time. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source. Learn more at https://flutter.dev

Ben Konyi

Written by

Ben Konyi

Google Software Engineer — Dart VM Hacker, Flutter Runtime Dev, Amateur Cyclist, and Proud Canadian 🍁

Flutter

Flutter is Google's mobile UI framework for crafting high-quality native interfaces on iOS and Android in record time. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source. Learn more at https://flutter.dev

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store