Flutter SDK Usage & Integration
In-SDK Geofencing is the recommended way to integrate: hand the SDK your stops and it starts and stops tracking automatically as the driver enters and exits each one. Prefer to drive geofence enter/exit yourself? See Manual Geofencing below.
In-SDK Geofencing
Hand the SDK your stops and it auto-starts and stops a tracking session as the driver enters and exits each one.
This flow has four touchpoints in your driver app (Initialize SDK is shared with the manual flow below):
| # | When this happens in the driver app | What you call |
|---|---|---|
| 1 | Driver app launches | Initialize SDK |
| 2 | Start geofencing and tracking all stops | Start Geofencing |
| 3 | Driver confirms drop-off or takes proof of delivery | Mark Drop-off |
| 4 | Stop geofencing and tracking all stops | Stop Geofencing |
If the driver's route changes mid-shift, call Update Stops as well.
Requires location permission. On Android, background operation also requires ACCESS_BACKGROUND_LOCATION ("Allow all the time"); without it, geofence registration is skipped entirely. On iOS the system prompts automatically for "Always" authorization, and route geofencing similarly won't arm without it. The SDK owns a cross-platform, typed permission flow — request foreground buckets first, then request background/Always location separately. See the dedicated Flutter Permissions guide.
How It Works
- You pass the route as a
List<DeliveryStop>; the SDK geofences the nearest stops for the route's lifetime. - On ENTER, it auto-starts a session for that stop (same as
startDeliveryByAddressString). - You call
markDropoffwhen the driver completes the stop. - On EXIT, the session stops only if
markDropoffwas called. Otherwise it runs untiltimeoutSeconds, so a false EXIT can't cut a delivery short. - Every auto-start/stop emits a
GeofenceSessionEventon thegeofenceSessionEventsstream.
The SDK handles the rest automatically: it absorbs GPS jitter (enter/exit hysteresis + debounce) and tightens location cadence near stops to balance precision and battery. These are tuned via remote config, not in your app.
API Reference
| Function | Description |
|---|---|
startRouteGeofencing({stops, options}) | Begins geofencing for stops (List<DeliveryStop>), tuned by options (RouteGeofenceOptions). Throws if stops is empty or the SDK isn't initialized/eligible |
markDropoff({deliveryId, dropoffType}) | Marks a stop's drop-off, gating its geofence EXIT stop. Shared with the manual flow (see Mark Drop-off) |
updateRouteStops({stops}) | Replaces the active stop set (List<DeliveryStop>) mid-route and re-diffs registration. A session for a removed stop is auto-stopped (tagged removed_from_route) |
stopRouteGeofencing() | Clears all geofences and stops active route sessions |
resumeRouteGeofencingIfNeeded() | Restores geofencing from persisted state. Call on every app launch; no-op if no route is active |
getMonitoredStops() | Returns the stops currently registered (List<DeliveryStop>, empty list if no route is active) |
geofenceSessionEvents | A Stream<GeofenceSessionEvent> of auto start/stop events |
1. Initialize SDK
Initialize the SDK once, early in your app's lifecycle. You can let the DoorstepAiView widget
handle it, or initialize manually for more control. DoorstepAI.init configures Android's
foreground-service notification and is a no-op on iOS.
Option A: DoorstepAiView widget
The widget initializes the SDK and requests permissions automatically. It is no longer required as of the latest release (manual init works just as well), but it's the quickest path:
import 'package:flutter/material.dart';
import 'package:doorstepai_dropoff_sdk/doorstepai_dropoff_sdk.dart';
import 'package:doorstepai_dropoff_sdk/doorstep_ai_view.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: DoorstepAiView(
apiKey: 'your-api-key',
notificationTitle: 'Tracking...',
notificationText: 'Tracking your delivery',
),
),
);
}
}
Option B: Manual initialization
For more control, initialize the SDK and set the API key yourself, then request permissions:
import 'package:doorstepai_dropoff_sdk/doorstepai_dropoff_sdk.dart';
class DeliveryManager {
static Future<void> initializeSDK() async {
// Initialize SDK first
await DoorstepAI.init(
notificationTitle: 'Tracking...',
notificationText: 'Tracking your delivery',
);
// Set API key after initialization
await DoorstepAI.setApiKey('your-api-key');
// Re-arms a persisted route; safe when no route is active.
await DoorstepAI.resumeRouteGeofencingIfNeeded();
// Trigger this from an appropriate, user-visible permission flow.
final foreground = await DoorstepAI.requestPermissions(
const PermissionRequestOptions(
permissions: [
DoorstepPermissionType.locationWhenInUse,
DoorstepPermissionType.motion,
DoorstepPermissionType.bluetooth,
DoorstepPermissionType.nearbyWifiDevices,
DoorstepPermissionType.notifications,
],
),
);
if (foreground.statuses[DoorstepPermissionType.locationWhenInUse]?.state ==
DoorstepPermissionState.granted) {
await DoorstepAI.requestBackgroundLocationPermission();
}
}
}
See Flutter Permissions for whitelist requests and the full result model.
Startup values
| Parameter | Type | Required | Description |
|---|---|---|---|
apiKey | String | Yes | Your DoorstepAI API key |
notificationTitle | String? | No | Title on the foreground-service notification shown while tracking |
notificationText | String? | No | Description on that notification |
DoorstepAI.isInitialized reflects whether Android init has completed. Because init is a
no-op on iOS, do not use this getter as a cross-platform readiness check; await your own startup
sequence instead.
Store your API key securely using environment variables, secure storage, or a configuration service. Never hardcode API keys in production builds.
2. Start Geofencing
Call startRouteGeofencing once you have the driver's stops for the shift. Pass every stop up front, not one at a time:
import 'package:doorstepai_dropoff_sdk/doorstepai_dropoff_sdk.dart';
final todaysStops = [
DeliveryStop(
deliveryId: 'delivery_12345',
address: '123 Main St, Apt 4B, San Francisco, CA 94102',
latitude: 37.7749,
longitude: -122.4194,
),
];
await DoorstepAI.startRouteGeofencing(
stops: todaysStops,
options: RouteGeofenceOptions(defaultRadiusMeters: 250),
);
3. Mark Drop-off
When the driver completes the delivery (takes a POD or confirms drop-off in-app), mark it. markDropoff is the preferred API:
The call is identical for both flows. With In-SDK Geofencing, marking drop-off also gates the automatic geofence EXIT stop for that delivery: the session keeps running until it's called (or the timeout backstop fires).
await DoorstepAI.markDropoff(
deliveryId: 'delivery_12345',
dropoffType: DropoffType.pod, // or DropoffType.nonPod
);
markDropoff parameters
| Parameter | Type | Description |
|---|---|---|
deliveryId | String | Required. The session you're marking |
dropoffType | DropoffType | pod (proof of delivery captured) or nonPod |
Custom events with newEvent
Beyond drop-offs, newEvent tags any custom event on the active session (for example, when the driver starts capturing a photo):
await DoorstepAI.newEvent(
eventName: 'taking_pod',
deliveryId: 'delivery_12345',
timestamp: 1720000000, // optional epoch seconds; omit for "now"
);
| Parameter | Type | Description |
|---|---|---|
eventName | String | Required. Name of the event to record on the session |
deliveryId | String | Required. The active session to attach the event to |
timestamp | double? | Epoch seconds for the event; omit it to use the current time |
4. Stop Geofencing
Call stopRouteGeofencing when the shift ends (or the driver logs out). It clears every geofence and stops any sessions still running:
await DoorstepAI.stopRouteGeofencing();
Update Stops
Call updateRouteStops when the driver's route changes mid-shift (a stop added, removed, or reordered). It re-diffs against the currently monitored stops instead of tearing everything down, and auto-stops a session for any stop dropped from the list, tagged removed_from_route:
await DoorstepAI.updateRouteStops(stops: updatedRouteStops); // List<DeliveryStop>
Resuming After Relaunch
Not part of the standard flow above, but worth adding for reliability: route state is persisted, so geofencing survives process death and OS restarts. Call resumeRouteGeofencingIfNeeded() on every app launch to restore it without re-supplying the stop list. It's a no-op if no route is active, so it's safe to call unconditionally:
class DeliveryManager {
static Future<void> initializeSDK() async {
await DoorstepAI.init(/* ... */);
await DoorstepAI.setApiKey('your-api-key');
await DoorstepAI.resumeRouteGeofencingIfNeeded();
}
}
Getting Monitored Stops
Call getMonitoredStops to read back the stops currently registered, useful for rehydrating your UI on launch or confirming a route is active:
final stops = await DoorstepAI.getMonitoredStops();
print('${stops.length} stop(s) currently monitored');
Types
DeliveryStop
A single stop on the route. deliveryId is the same id you pass to markDropoff and stopDelivery.
class DeliveryStop {
final String deliveryId;
final String address;
final double latitude;
final double longitude;
final double? radiusMeters;
final String? customerId;
final String? driverId;
}
| Field | Type | Required | Description |
|---|---|---|---|
deliveryId | String | Yes | Your id for the stop. Pass the same id to markDropoff/stopDelivery. Duplicates are de-duped (last wins) |
address | String | Yes | Passed verbatim to session creation |
latitude | double | Yes | Stop latitude (geofence center) |
longitude | double | Yes | Stop longitude (geofence center) |
radiusMeters | double? | No | Per-stop geofence radius. Falls back to RouteGeofenceOptions.defaultRadiusMeters / remote config when omitted |
customerId | String? | No | Forwarded to session creation for correlation on your backend |
driverId | String? | No | Forwarded to session creation for correlation on your backend |
RouteGeofenceOptions
Tuning for the whole route. Every field has a default, so startRouteGeofencing(stops: stops) works with no options.
class RouteGeofenceOptions {
final double defaultRadiusMeters;
final int? timeoutSeconds;
final int? autoStopAfterDropoffSeconds;
final bool? manualForeground; // Android only
const RouteGeofenceOptions({
this.defaultRadiusMeters = 250,
this.timeoutSeconds,
this.autoStopAfterDropoffSeconds,
this.manualForeground,
});
}
| Field | Type | Default | Description |
|---|---|---|---|
defaultRadiusMeters | double | 250 | Geofence radius for stops that don't set their own radiusMeters |
timeoutSeconds | int? | null | Backstop for each auto-started session: it stops this long after starting even if no EXIT/dropoff arrives. null uses remote config |
autoStopAfterDropoffSeconds | int? | null | Forwarded to the post-dropoff auto-stop, same as startDelivery*'s autoStopAfterDropoffSeconds |
manualForeground | bool? | null | (Android only) When true, your app owns the foreground service and the SDK won't promote its own |
GeofenceSessionEvent
Emitted on geofenceSessionEvents whenever the SDK auto-starts or auto-stops a session in response to a geofence transition.
enum GeofenceSessionEventType { started, stopped }
class GeofenceSessionEvent {
final String deliveryId;
final GeofenceSessionEventType type;
final String reason;
}
| Field | Type | Description |
|---|---|---|
deliveryId | String | The stop's DeliveryStop.deliveryId |
type | GeofenceSessionEventType | started (session auto-started on ENTER) or stopped (session auto-stopped) |
reason | String | What triggered it: geofence, distance, timeout, removed_from_route, route_cleared, or manual |
Observing Geofencing Events
Listen to geofenceSessionEvents to subscribe to auto start/stop events so your UI can reflect them in real time:
final subscription = DoorstepAI.geofenceSessionEvents.listen((event) {
print('Geofencing event: ${event.deliveryId} -> ${event.type} (${event.reason})');
});
// Later:
await subscription.cancel();
Manual Geofencing
If you'd rather drive geofence enter/exit yourself instead of using In-SDK Geofencing above, wire up these four touchpoints:
The DoorstepAI SDK has four touchpoints in your driver app. Wire up each one and you're done. Everything else on this page is reference detail for those four calls.
| # | When this happens in the driver app | What you call |
|---|---|---|
| 1 | App launches | Initialize SDK |
| 2 | Driver enters the ≥250 m delivery geofence | Start Tracking |
| 3 | Driver takes POD or confirms drop-off in-app | Mark Drop-off |
| 4 | Driver exits the ≥250 m delivery geofence | Stop Tracking |
Tracking needs runtime location permission on Android. Request it during init (see Manual Initialization and Permission Handling). On iOS the system prompts automatically.
1. Initialize SDK
Initialize the SDK the same way as described in Initialize SDK above. This step is identical for both flows.
2. Start Tracking
Call startDelivery… when the driver enters the delivery geofence around the target building, not once inside. Use a unique deliveryId per session: a meaningful identifier you can correlate on your side. Wrap calls in try/catch to handle failures such as an invalid key or denied permissions.
Pick whichever address format you have (startDeliveryByPlaceID, startDeliveryByAddress, or startDeliveryByAddressString):
startDeliveryByAddress is an exact convenience alias for startDeliveryByAddressType; both
accept the same named parameters.
import 'package:doorstepai_dropoff_sdk/doorstepai_dropoff_sdk.dart';
// By Google Place ID
await DoorstepAI.startDeliveryByPlaceID(
placeID: 'some_place_id',
deliveryId: 'delivery_12345',
);
// By address components
final address = AddressType(
streetNumber: '123',
route: 'Main Street',
subPremise: 'Apt 4B',
locality: 'San Francisco',
administrativeAreaLevel1: 'CA',
postalCode: '94102',
);
await DoorstepAI.startDeliveryByAddress(
address: address,
deliveryId: 'delivery_12345',
);
// By single address string, with optional coordinates and knobs
await DoorstepAI.startDeliveryByAddressString(
address: '123 Main St, Apt 4B, San Francisco, CA 94102',
deliveryId: 'delivery_12345',
coordinates: LatLngObject(lat: 37.7749, lng: -122.4194),
timeoutSeconds: 1200,
);
Each start resolves to String?: Android returns the server-assigned session id when one was created. Native iOS starts are asynchronous and return null; subscribe to DoorstepAI.sessionServerIdAssignedEvents when you need the iOS id.
Start parameters
Every startDelivery* method requires deliveryId and accepts timeoutSeconds, customerId, and
driverId. The remaining parameters are variant-specific:
| Parameter | Type | Description |
|---|---|---|
deliveryId | String | Required. Unique per session; correlate it on your side |
timeoutSeconds | double? | Auto-stops tracking after this duration, a backstop if the exit geofence is missed |
manualForeground | bool | (Place ID and address variants; Android only; default false) When true, the SDK won't promote its tracking service to the foreground; your app must already run its own |
coordinates | LatLngObject? | (address, addressType, and addressString only) pairs a textual address with a lat/lng you resolved upstream |
customerId | String? | Optional customer identifier forwarded to session creation |
driverId | String? | Optional driver identifier forwarded to session creation |
Deprecated start methods
startDeliveryByPlusCode and startDeliveryByLatLng are deprecated. Use startDeliveryByAddressString / startDeliveryByAddress with coordinates instead. They remain for backwards compatibility:
await DoorstepAI.startDeliveryByPlusCode(
plusCode: 'some_plus_code',
deliveryId: 'delivery_12345',
);
await DoorstepAI.startDeliveryByLatLng(
latitude: 37.7749,
longitude: -122.4194,
subUnit: 'Apt 4B',
deliveryId: 'delivery_12345',
);
On Android, the deprecated Plus Code and lat/lng native overloads do not support
manualForeground. Use the supported place/address methods when you need that option.
3. Mark Drop-off
Mark the drop-off the same way as described in Mark Drop-off above. This step is identical for both flows. newEvent there also accepts an optional timestamp (epoch seconds) to backdate a custom event; omit it to use the current time.
4. Stop Tracking
Call stopDelivery when the driver exits the delivery geofence:
await DoorstepAI.stopDelivery(deliveryId: 'delivery_12345');
That covers the full lifecycle. The sections below are reference for the calls above.
Best Practices
Error Handling
Platform failures are rethrown as DoorstepAIException, with code, message, and optional
details. Always implement error handling for SDK methods:
Future<void> startDeliveryWithErrorHandling() async {
try {
await DoorstepAI.startDeliveryByPlaceID(
placeID: 'your-place-id',
deliveryId: 'unique-delivery-id',
);
// Handle success
} catch (error) {
// Handle error
print('Error: $error');
}
}
Lifecycle Management
The SDK keeps tracking in the background, so you usually don't need to react to app-state changes. If you want to observe them, use a WidgetsBindingObserver:
import 'package:flutter/material.dart';
class DeliveryManager extends StatefulWidget {
@override
_DeliveryManagerState createState() => _DeliveryManagerState();
}
class _DeliveryManagerState extends State<DeliveryManager> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
print('App paused - SDK continues tracking in background');
break;
case AppLifecycleState.resumed:
print('App resumed');
break;
default:
break;
}
}
}
Permission Handling
DoorstepAiView uses this API automatically. For a custom permission screen, pass a true whitelist: omit PermissionRequestOptions to request every platform bucket, pass a list to request exactly those buckets, or pass an empty list to request nothing.
final foreground = await DoorstepAI.requestPermissions(
const PermissionRequestOptions(
permissions: [
DoorstepPermissionType.locationWhenInUse,
DoorstepPermissionType.motion,
DoorstepPermissionType.bluetooth,
DoorstepPermissionType.nearbyWifiDevices,
DoorstepPermissionType.notifications,
],
),
);
if (foreground.statuses[DoorstepPermissionType.locationWhenInUse]?.state ==
DoorstepPermissionState.granted) {
await DoorstepAI.requestBackgroundLocationPermission();
}
final current = await DoorstepAI.checkPermissions();
The six buckets are locationWhenInUse, locationAlways, motion, bluetooth, nearbyWifiDevices, and notifications. Detailed states are granted, notDetermined, whenInUseOnly, denied, permanentlyDenied, restricted, unavailable, and notDeclared. The request result also reports requested, alreadyDetermined, unavailable, and deferred buckets.
On both platforms, request locationAlways only after foreground location is granted. Prefer a user-initiated permission flow on iOS. The Wi-Fi and notification buckets report unavailable there because those runtime grants are Android-only.
For exact signatures, result types, platform mappings, Android-only helpers, and troubleshooting, see Flutter Permissions.
Next Steps
- 🔐 Review Permissions: buckets, states, and staged prompts
- 💡 View Examples: complete implementation examples
- 🛠️ Troubleshooting Guide: common integration and runtime issues