Skip to content

Quickstart

This assumes the package is already installed (see Installation) and that your app’s manifest/Info.plist already declare the permissions covered in the previous page, Permissions & background location — the SDK cannot request a permission the app hasn’t declared.

A complete main.dart

lib/main.dart
import 'package:bgeo_background_geolocation/bgeo_background_geolocation.dart' as bg;
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: HomePage());
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
// Subscribe BEFORE ready() so no early location/motionchange event —
// including the initial `isMoving` probe ready() may fire while
// resuming an already-enabled session — is missed.
bg.BackgroundGeolocation.onLocation((location) {
debugPrint('[location] ${location.coords} moving: ${location.isMoving}');
});
bg.BackgroundGeolocation.onMotionChange((event) {
debugPrint('[motionchange] ${event.isMoving} ${event.location?.coords}');
});
final state = await bg.BackgroundGeolocation.ready(bg.Config(
desiredAccuracy: bg.desiredAccuracyHigh,
distanceFilter: 30, // metres between points while moving
// stopTimeout is in MINUTES, not seconds (see the Config reference) —
// 5 minutes of continuous stillness before the engine commits to
// stationary. This is also the SDK default; spelled out here for clarity.
stopTimeout: 5,
url: 'https://your-server.example/locations',
logLevel: bg.logLevelInfo,
stopOnTerminate: false, // keep tracking after the app process is killed
startOnBoot: true, // resume tracking after a device reboot
));
if (!state.enabled) {
// requestPermission() runs the staged permission flow (Android:
// fine -> background -> activity recognition; iOS: location
// authorization) — see the Permissions page for what each platform
// needs declared before this resolves with anything but denied.
final status = await bg.BackgroundGeolocation.requestPermission();
if (status == bg.authorizationStatusAlways) {
await bg.BackgroundGeolocation.start();
} else {
// Denied, restricted, or When-In-Use-only — don't start tracking.
// Prompt the user back through your own onboarding/Settings nudge.
debugPrint('[bgeo] insufficient location authorization (status: $status) — not starting');
}
}
}
@override
void dispose() {
bg.BackgroundGeolocation.removeListeners();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: Text('BGeo is tracking in the background.')),
);
}
}

What you should see

  1. Permission prompts. On first launch, requestPermission() walks through the OS dialogs — Android’s staged “While using the app” → “Allow all the time” → activity-recognition prompts, or iOS’s When In Use / Always dialog. See Permissions & background location for the manifest/Info.plist declarations these prompts depend on — without them, the OS denies the request outright rather than showing a dialog. If the resolved status isn’t authorizationStatusAlways, the code above logs a warning and skips start() instead of tracking with insufficient authorization.
  2. A motionchange event. Once tracking starts, walk (or drive) away from where the device was resting. You’ll see [motionchange] true logged as the motion state machine decides the device is moving; standing still again for the configured stopTimeout logs [motionchange] false once it commits back to stationary.
  3. location events. While moving, [location] logs fire roughly every distanceFilter metres, each with location.coords and the current isMoving verdict.
  4. Queue draining, or 401s. Locations upload natively as they’re queued. Point url at a real endpoint and you’ll see them arrive server-side with no Dart involvement; point it at a placeholder (as above) and you’ll instead see repeated connection failures/401s in the log as the queue keeps retrying — that’s expected until a real server is listening. See the HTTP guide for batching, retries, and JWT-refresh once you’re pointing at a real backend.
The example app's Logs tab on iOS and Android, showing a run of tracking log lines: track.start, motion.moving, onLocation with coordinates, and onHttp 200 responses.

The same lines land in your terminal via flutter run; the example app’s on-screen log (see the example app guide) shows them on the device itself, interleaved with the native engine’s own rows — see the logging guide.

Common pitfalls