locus 1.0.0 copy "locus: ^1.0.0" to clipboard
locus: ^1.0.0 copied to clipboard

Background geolocation SDK for Flutter. Native tracking, geofencing, activity recognition, and sync.

Locus Background Geolocation #

CI License: Locus Community

A Flutter background geolocation SDK for Android and iOS. This package provides continuous location tracking, motion/activity updates, geofencing, schedule-based tracking, and HTTP auto-sync so you can build apps with background location features without a paid license.


Features #

  • Continuous location tracking with motion state changes.
  • Activity recognition updates (walking, running, in-vehicle, etc.).
  • Geofencing with enter/exit events and stored geofence management.
  • Configurable accuracy, distance filters, and update intervals.
  • Optional HTTP auto-sync with custom headers and params.
  • Batch sync with persisted locations when enabled.
  • Configurable HTTP retries with exponential backoff.
  • Odometer tracking and basic log capture.
  • Foreground service notification controls on Android.
  • Connectivity, power-save, enabled-change, and geofences-change events.
  • Heartbeat events (interval-based) and schedule windows (HH:mm-HH:mm).
  • Headless execution with start-on-boot (Android) and background relaunch handling.
  • Motion tuning: stationary radius and motion trigger/stop timeouts.
  • Log levels with basic retention (logMaxDays).
  • Activity filters (triggerActivities), and state diagnostics via getState().
  • Config presets for common tracking profiles.
  • Location anomaly detection helpers for implausible jumps.
  • Offline-first queue for custom payload sync with idempotency.
  • Trip lifecycle events (start/update/end) with route deviation detection.
  • Adaptive tracking profiles for job states (off-duty/standby/en-route/arrived).
  • Geofence workflows with sequencing and cooldown enforcement.
  • Diagnostics snapshot and remote command helpers.
  • Location quality scoring with spoof-suspicion flags.
  • Battery optimization with adaptive tracking, sync policies, and power state monitoring.
  • Speed-based GPS tuning to reduce updates when stationary or walking.
  • Network-aware sync policies for WiFi, cellular, and metered connections.
  • Battery benchmarking for measuring power consumption during testing.
  • Enhanced spoof detection with multi-factor analysis and confidence scoring.
  • Significant location changes for ultra-low power monitoring (~500m movements).
  • Error recovery API with automatic retries, exponential backoff, and custom handlers.

Platform Support #

Platform Minimum Version
Android API 26 (Android 8.0)
iOS iOS 14.0

Installation #

Add this to your pubspec.yaml:

dependencies:
  locus: ^1.0.0

Then run flutter pub get.

Quick Setup with CLI #

After adding the package, run the setup wizard to automatically configure your Android and iOS project:

dart run locus:setup

To verify your configuration is correct:

dart run locus:doctor

If issues are found, the doctor can auto-fix most of them:

dart run locus:doctor --fix

CLI Tools #

Locus includes command-line tools to simplify project setup and debugging:

Setup Wizard #

dart run locus:setup [options]

Options:
  --android-only       Only configure Android
  --ios-only           Only configure iOS
  --with-activity      Include activity recognition permissions
  -h, --help           Show usage information

The setup wizard automatically:

  • Adds required permissions to AndroidManifest.xml
  • Configures Info.plist with location usage descriptions
  • Adds UIBackgroundModes for background location
  • Checks minSdkVersion and iOS deployment target

Doctor Command #

dart run locus:doctor [options]

Options:
  --fix                Attempt to auto-fix any issues found
  -h, --help           Show usage information

The doctor command checks:

  • All required Android permissions
  • Android minSdkVersion >= 26
  • iOS location usage description keys
  • iOS UIBackgroundModes contains location
  • iOS deployment target >= 14.0

Configuration #

Android #

The plugin declares required permissions and components via manifest merging. For Android 10+ you must request runtime background location and activity recognition permissions.

If you need to override or explicitly declare them, add to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

iOS #

Add the following keys to ios/Runner/Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to track your route.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs background location access to track your route.</string>
<key>NSMotionUsageDescription</key>
<string>This app uses motion data to improve activity detection.</string>
<key>UIBackgroundModes</key>
<array>
  <string>location</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
  <string>dev.locus.motionDetector.refresh</string>
</array>

If you override bgTaskId in Config, make sure the same identifier is listed in BGTaskSchedulerPermittedIdentifiers.


Usage #

1. Request Permissions #

import 'package:locus/locus.dart';

final granted = await Locus.requestPermission();
if (!granted) {
  // Handle denied permissions.
}

2. Configure and Start Tracking #

final state = await Locus.ready(const Config(
  desiredAccuracy: DesiredAccuracy.high,
  distanceFilter: 25,
  heartbeatInterval: 60,
  activityRecognitionInterval: 10000,
  stopTimeout: 5,
  stationaryRadius: 25,
  motionTriggerDelay: 15000,
  enableHeadless: true,
  startOnBoot: true,
  stopOnTerminate: false,
  autoSync: true,
  disableAutoSyncOnCellular: true,
  maxRetry: 3,
  retryDelay: 5000,
  retryDelayMultiplier: 2.0,
  maxRetryDelay: 60000,
  logLevel: LogLevel.info,
  logMaxDays: 7,
  url: 'https://example.com/locations',
  notification: NotificationConfig(
    title: 'Tracking enabled',
    text: 'Locus is tracking your location',
    actions: ['PAUSE', 'STOP'],
  ),
));

if (!state.enabled) {
  await Locus.start();
}

2a. Use Config Presets #

final config = ConfigPresets.tracking.copyWith(
  url: 'https://example.com/locations',
  notification: const NotificationConfig(
    title: 'Tracking enabled',
    text: 'Locus is tracking your location',
  ),
);
await Locus.ready(config);

3. Subscribe to Events #

final locationSub = Locus.onLocation((location) {
  print('Location: ${location.coords.latitude}, ${location.coords.longitude}');
});

final motionSub = Locus.onMotionChange((location) {
  print('Motion change: ${location.isMoving}');
});

final providerSub = Locus.onProviderChange((event) {
  print('Provider: ${event.authorizationStatus}');
});

3a. Detect Location Anomalies #

Locus.onLocationAnomaly((anomaly) {
  print('Anomalous speed: ${anomaly.speedKph} kph');
});

3b. Queue Custom Payloads #

final id = await Locus.enqueue({
  'event': 'tripstart',
  'tripId': 'trip-123',
});

await Locus.syncQueue();

3c. Trip Lifecycle Events #

await Locus.startTrip(TripConfig(
  startOnMoving: true,
  route: const [
    RoutePoint(latitude: 37.42, longitude: -122.08),
    RoutePoint(latitude: 37.43, longitude: -122.09),
  ],
));

Locus.onTripEvent((event) {
  print('Trip event: ${event.type}');
});

3d. Adaptive Tracking Profiles #

await Locus.setTrackingProfiles(
  {
    TrackingProfile.offDuty: ConfigPresets.lowPower,
    TrackingProfile.standby: ConfigPresets.balanced,
    TrackingProfile.enRoute: ConfigPresets.tracking,
    TrackingProfile.arrived: ConfigPresets.trail,
  },
  initialProfile: TrackingProfile.standby,
);

await Locus.setTrackingProfile(TrackingProfile.enRoute);

3e. Geofence Workflows #

Locus.registerGeofenceWorkflows(const [
  GeofenceWorkflow(
    id: 'pickup_dropoff',
    steps: [
      GeofenceWorkflowStep(
        id: 'pickup',
        geofenceIdentifier: 'pickup_zone',
        action: GeofenceAction.enter,
      ),
      GeofenceWorkflowStep(
        id: 'dropoff',
        geofenceIdentifier: 'dropoff_zone',
        action: GeofenceAction.enter,
      ),
    ],
  ),
]);

Locus.onWorkflowEvent((event) {
  print('Workflow ${event.workflowId} ${event.status}');
});

3f. Diagnostics + Remote Commands #

final snapshot = await Locus.getDiagnostics();
print(snapshot.toMap());

await Locus.applyRemoteCommand(
  RemoteCommand(
    id: 'cmd-1',
    type: RemoteCommandType.syncQueue,
  ),
);

3g. Location Quality Scoring #

Locus.onLocationQuality((quality) {
  print('Quality score: ${quality.overallScore}');
  if (quality.isSpoofSuspected) {
    print('Potential spoof detected');
  }
});

4. Geofencing #

await Locus.addGeofence(const Geofence(
  identifier: 'home',
  radius: 100,
  latitude: 37.4219983,
  longitude: -122.084,
  notifyOnEntry: true,
  notifyOnExit: true,
));

final geofenceSub = Locus.onGeofence((event) {
  print('Geofence ${event.geofence.identifier} ${event.action}');
});

5. Stop Tracking #

await Locus.stop();
await locationSub.cancel();

6. Schedule Windows #

Schedule strings are in HH:mm-HH:mm format (24-hour), and can span midnight.

await Locus.ready(const Config(
  schedule: ['08:00-12:00', '13:00-18:00'],
));
await Locus.startSchedule();

7. Headless Tasks (Android) #

Register a top-level callback to receive events while the app is terminated.

@pragma('vm:entry-point')
Future<void> backgroundGeolocationHeadlessTask(HeadlessEvent event) async {
  if (event.name == 'boot') {
    await Locus.ready(const Config(
      enableHeadless: true,
      startOnBoot: true,
      stopOnTerminate: false,
    ));
    await Locus.start();
  }
}

await Locus.registerHeadlessTask(
  backgroundGeolocationHeadlessTask,
);

8. Background Tasks #

final taskId = await Locus.startBackgroundTask();
// Do short background work here.
await Locus.stopBackgroundTask(taskId);

9. Stored Locations #

final stored = await Locus.getLocations(limit: 50);
await Locus.destroyLocations();

10. Diagnostics #

final state = await Locus.getState();

API Reference #

For detailed documentation of all classes and methods, please refer to the official documentation.

Core Classes #

  • Locus: Main SDK entry point.
  • Config: Global configuration options.
  • Location: Recorded location data point.
  • Geofence: Geofence definition and state.
  • TripConfig: configuration for the Trip Lifestyle engine.
  • MockLocus: Testing mock for unit tests.
  • ConfigValidator: Configuration validation utility.

Testing #

Locus provides testing utilities to enable unit testing without platform channels.

MockLocus #

import 'package:locus/locus.dart';

void main() {
  late MockLocus mock;

  setUp(() {
    mock = MockLocus();
  });

  tearDown(() {
    mock.dispose();
  });

  test('handles location updates', () async {
    final locations = <Location>[];
    mock.locationStream.listen(locations.add);

    // Emit a mock location
    mock.emitLocation(MockLocationExtension.mock(
      latitude: 37.4219,
      longitude: -122.084,
      speed: 15.5,
    ));

    await Future.delayed(Duration.zero);
    expect(locations.length, 1);
    expect(locations.first.coords.latitude, 37.4219);
  });

  test('tracks method calls', () async {
    await mock.ready(const Config());
    await mock.start();
    await mock.getCurrentPosition();

    expect(mock.methodCalls, ['ready', 'start', 'getCurrentPosition']);
  });
}

Mock Extensions #

// Create mock locations easily
final location = MockLocationExtension.mock(
  latitude: 40.7128,
  longitude: -74.006,
  activityType: ActivityType.inVehicle,
);

// Create mock geofences
final geofence = MockGeofenceExtension.mock(
  identifier: 'home',
  latitude: 37.4219,
  longitude: -122.084,
  radius: 100,
);

Debug Overlay #

Add a visual debug overlay during development to monitor location tracking:

import 'package:flutter/foundation.dart';
import 'package:locus/locus.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Stack(
        children: [
          YourMainWidget(),
          // Only show in debug mode
          if (kDebugMode)
            const LocusDebugOverlay(
              position: DebugOverlayPosition.bottomRight,
              expanded: false,
            ),
        ],
      ),
    );
  }
}

The debug overlay shows:

  • Tracking status (ON/OFF)
  • Current location coordinates
  • Accuracy, speed, heading, altitude
  • Activity recognition state
  • Odometer distance
  • Recent location history
  • Start/Stop controls

State-Agnostic Streams #

Locus provides stream getters that work with any state management solution:

With Riverpod #

final locationProvider = StreamProvider.autoDispose((ref) {
  return Locus.locationStream;
});

final activityProvider = StreamProvider.autoDispose((ref) {
  return Locus.activityStream;
});

With BLoC #

class LocationBloc extends Bloc<LocationEvent, LocationState> {
  StreamSubscription? _subscription;

  LocationBloc() : super(LocationInitial()) {
    _subscription = Locus.locationStream.listen((location) {
      emit(LocationLoaded(location));
    });
  }

  @override
  Future<void> close() {
    _subscription?.cancel();
    return super.close();
  }
}

Available Streams #

Stream Type Description
locationStream Stream<Location> Location updates
motionChangeStream Stream<Location> Motion state changes
activityStream Stream<Activity> Activity recognition
geofenceStream Stream<GeofenceEvent> Geofence crossings
providerStream Stream<ProviderChangeEvent> GPS/authorization changes
connectivityStream Stream<ConnectivityChangeEvent> Network changes
heartbeatStream Stream<Location> Heartbeat pings
httpStream Stream<HttpEvent> HTTP sync events
enabledStream Stream<bool> Tracking enabled/disabled
powerSaveStream Stream<bool> Power save mode changes

Dynamic HTTP Headers #

Add authentication tokens or session IDs that change at runtime:

// Set a callback that provides fresh headers before each request
Locus.setHeadersCallback(() async {
  final token = await authService.getAccessToken();
  return {
    'Authorization': 'Bearer $token',
    'X-Session-Id': sessionId,
    'X-Device-Id': deviceId,
  };
});

// Manually refresh headers after login/token refresh
await Locus.refreshHeaders();

// Clear the callback on logout
Locus.clearHeadersCallback();

Config Validation #

Validate configurations before applying them:

final config = Config(
  distanceFilter: -10, // Invalid!
  autoSync: true,      // No URL set!
);

final result = ConfigValidator.validate(config);
if (!result.isValid) {
  for (final error in result.errors) {
    print('${error.field}: ${error.message}');
    if (error.suggestion != null) {
      print('  Suggestion: ${error.suggestion}');
    }
  }
}

// Or throw on invalid config
try {
  ConfigValidator.assertValid(config);
} on ConfigValidationException catch (e) {
  print('Invalid config: $e');
}

Battery Optimization #

Locus includes comprehensive battery optimization features to minimize power consumption while maintaining tracking quality.

Adaptive Tracking #

Automatically adjusts GPS settings based on speed, battery level, and motion state:

// Enable adaptive tracking
await Locus.setAdaptiveTracking(AdaptiveTrackingConfig.balanced);

// Or use aggressive power saving
await Locus.setAdaptiveTracking(AdaptiveTrackingConfig.aggressive);

// Custom configuration
await Locus.setAdaptiveTracking(AdaptiveTrackingConfig(
  enabled: true,
  speedTiers: SpeedTiers.driving,
  batteryThresholds: BatteryThresholds.conservative,
  stationaryGpsOff: true,
  stationaryDelay: Duration(seconds: 30),
  smartHeartbeat: true,
));

Speed-Based Tuning #

GPS polling frequency adjusts based on current speed:

Speed (km/h) Update Interval Distance Filter Rationale
0 (stationary) 60s 50m Minimal movement
<5 (walking) 20s 15m Slow movement
5-30 (city) 10s 10m Turns, stops
30-80 (suburban) 7s 15m Consistent movement
>80 (highway) 5s 25m Need route accuracy

Sync Policies #

Control when location data is synchronized based on network and battery:

// Use a preset policy
await Locus.setSyncPolicy(SyncPolicy.balanced);

// Custom policy
await Locus.setSyncPolicy(SyncPolicy(
  onWifi: SyncBehavior.immediate,
  onCellular: SyncBehavior.batch,
  onMetered: SyncBehavior.manual,
  batchSize: 50,
  batchInterval: Duration(minutes: 5),
  lowBatteryThreshold: 20,
  lowBatteryBehavior: SyncBehavior.manual,
));

Power State Monitoring #

React to battery and charging state changes:

// Get current power state
final power = await Locus.getPowerState();
print('Battery: ${power.batteryLevel}%');
print('Charging: ${power.isCharging}');
print('Power save: ${power.isPowerSaveMode}');

// Listen to power state changes
Locus.onPowerStateChange((event) {
  if (event.current.isCriticalBattery) {
    Locus.stop();
  }
});

Battery Statistics #

Monitor tracking impact on battery:

final stats = await Locus.getBatteryStats();
print('GPS active: ${stats.gpsOnTimePercent.toStringAsFixed(1)}%');
print('Updates: ${stats.locationUpdatesCount}');
print('Drain rate: ${stats.estimatedDrainPerHour}%/hr');

Battery Benchmarking #

Compare battery usage between configurations:

await Locus.startBatteryBenchmark();
// ... run tracking test ...
final result = await Locus.stopBatteryBenchmark();
print('Drain per hour: ${result?.drainPerHour.toStringAsFixed(1)}%/hr');

Advanced Features #

Spoof Detection #

Multi-factor detection of mock/spoofed locations:

// Enable spoof detection
await Locus.setSpoofDetection(SpoofDetectionConfig.balanced);

// High security mode
await Locus.setSpoofDetection(SpoofDetectionConfig(
  enabled: true,
  blockMockLocations: true,
  sensitivity: SpoofSensitivity.high,
  onSpoofDetected: (event) {
    logSecurityEvent('Spoof detected: ${event.factors}');
  },
));

// Manually analyze a location
final event = Locus.analyzeForSpoofing(location, isMockProvider: false);
if (event != null) {
  print('Confidence: ${event.confidence}');
  print('Factors: ${event.factors.map((f) => f.description)}');
}

Detection factors include: mock provider, impossible speed, altitude anomalies, repeated coordinates, timestamp mismatches, and more.

Significant Location Changes #

Ultra-low power monitoring for large movements (~500m):

// Start monitoring
await Locus.startSignificantChangeMonitoring(
  SignificantChangeConfig(
    minDisplacementMeters: 500,
    onSignificantChange: (location) {
      print('Significant move: ${location.coords.latitude}');
    },
  ),
);

// Or use preset for maximum battery savings
await Locus.startSignificantChangeMonitoring(
  SignificantChangeConfig.ultraLowPower,
);

// Check status
if (Locus.isSignificantChangeMonitoringActive) {
  // Listen to stream
  Locus.significantChangeStream?.listen((event) {
    print('Moved ${event.displacementMeters}m');
  });
}

// Stop monitoring
await Locus.stopSignificantChangeMonitoring();

Error Recovery #

Centralized error handling with automatic retries:

// Configure error handling
Locus.setErrorHandler(ErrorRecoveryConfig(
  maxRetries: 3,
  retryDelay: Duration(seconds: 5),
  retryBackoff: 2.0,
  onError: (error, context) {
    if (error.type == LocusErrorType.permissionDenied) {
      showPermissionDialog();
      return RecoveryAction.requestUserAction;
    }
    return error.suggestedRecovery ?? RecoveryAction.retry;
  },
  onExhausted: (error) {
    analytics.logError(error);
  },
));

// Handle errors manually
try {
  await Locus.start();
} catch (e) {
  final action = await Locus.handleError(LocusError.fromException(e));
  if (action == RecoveryAction.retry) {
    // Retry logic
  }
}

// Listen to error stream
Locus.errorStream?.listen((error) {
  print('Error: ${error.type} - ${error.message}');
});

Error types: permissionDenied, servicesDisabled, locationTimeout, networkError, serviceDisconnected, configError, and more.


Example #

A complete example application is available in the example directory, demonstrating:

  • Real-time location updates
  • Geofence management
  • Trip tracking
  • Configuration UI
  • Log viewing

To run the example:

cd example
flutter run

Contributing #

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.


Security #

For security policy and vulnerability reporting, please see SECURITY.md.


License #

Locus Community License v1.0 (see LICENSE and doc/LICENSING.md).

Licensing summary

  • Individuals: free to use for any purpose, including commercial and closed-source.
  • Enterprises (USD 250k+ revenue): free only if the part of the product that includes or links to this package is open-sourced under an OSI-approved license.
  • Closed-source enterprise use requires a commercial license (contact: hello@mkoksal.dev).
0
likes
0
points
250
downloads

Publisher

verified publisherweorbis.com

Weekly Downloads

Background geolocation SDK for Flutter. Native tracking, geofencing, activity recognition, and sync.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

args, flutter, permission_handler, xml

More

Packages that depend on locus

Packages that implement locus