Skip to main content

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 appWhat you call
1Driver app launchesInitialize SDK
2Start geofencing and tracking all stopsStart Geofencing
3Driver confirms drop-off or takes proof of deliveryMark Drop-off
4Stop geofencing and tracking all stopsStop Geofencing

If the driver's route changes mid-shift, call Update Stops as well.

Permissions

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

  1. You pass the route as a List<DeliveryStop>; the SDK geofences the nearest stops for the route's lifetime.
  2. On ENTER, it auto-starts a session for that stop (same as startDeliveryByAddressString).
  3. You call markDropoff when the driver completes the stop.
  4. On EXIT, the session stops only if markDropoff was called. Otherwise it runs until timeoutSeconds, so a false EXIT can't cut a delivery short.
  5. Every auto-start/stop emits a GeofenceSessionEvent on the geofenceSessionEvents stream.

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

FunctionDescription
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)
geofenceSessionEventsA 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:

main.dart
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:

delivery_manager.dart
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

ParameterTypeRequiredDescription
apiKeyStringYesYour DoorstepAI API key
notificationTitleString?NoTitle on the foreground-service notification shown while tracking
notificationTextString?NoDescription 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.

API Key Security

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

ParameterTypeDescription
deliveryIdStringRequired. The session you're marking
dropoffTypeDropoffTypepod (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"
);
ParameterTypeDescription
eventNameStringRequired. Name of the event to record on the session
deliveryIdStringRequired. The active session to attach the event to
timestampdouble?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;
}
FieldTypeRequiredDescription
deliveryIdStringYesYour id for the stop. Pass the same id to markDropoff/stopDelivery. Duplicates are de-duped (last wins)
addressStringYesPassed verbatim to session creation
latitudedoubleYesStop latitude (geofence center)
longitudedoubleYesStop longitude (geofence center)
radiusMetersdouble?NoPer-stop geofence radius. Falls back to RouteGeofenceOptions.defaultRadiusMeters / remote config when omitted
customerIdString?NoForwarded to session creation for correlation on your backend
driverIdString?NoForwarded 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,
});
}
FieldTypeDefaultDescription
defaultRadiusMetersdouble250Geofence radius for stops that don't set their own radiusMeters
timeoutSecondsint?nullBackstop for each auto-started session: it stops this long after starting even if no EXIT/dropoff arrives. null uses remote config
autoStopAfterDropoffSecondsint?nullForwarded to the post-dropoff auto-stop, same as startDelivery*'s autoStopAfterDropoffSeconds
manualForegroundbool?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;
}
FieldTypeDescription
deliveryIdStringThe stop's DeliveryStop.deliveryId
typeGeofenceSessionEventTypestarted (session auto-started on ENTER) or stopped (session auto-stopped)
reasonStringWhat 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 appWhat you call
1App launchesInitialize SDK
2Driver enters the ≥250 m delivery geofenceStart Tracking
3Driver takes POD or confirms drop-off in-appMark Drop-off
4Driver exits the ≥250 m delivery geofenceStop Tracking
Permissions

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:

ParameterTypeDescription
deliveryIdStringRequired. Unique per session; correlate it on your side
timeoutSecondsdouble?Auto-stops tracking after this duration, a backstop if the exit geofence is missed
manualForegroundbool(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
coordinatesLatLngObject?(address, addressType, and addressString only) pairs a textual address with a lat/lng you resolved upstream
customerIdString?Optional customer identifier forwarded to session creation
driverIdString?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

  1. 🔐 Review Permissions: buckets, states, and staged prompts
  2. 💡 View Examples: complete implementation examples
  3. 🛠️ Troubleshooting Guide: common integration and runtime issues