I Open-Sourced a Background Geolocation Plugin for Flutter.
Motion-detection, geofencing, crash detection, and on-device AI — for free. No $500 license, no “contact sales,” no asterisks.
I Open-Sourced a Background Geolocation Plugin for Flutter. It Just Crossed 7,200 Downloads in 30 Days.
Motion-detection, geofencing, crash detection, and on-device AI — for free. No $500 license, no “contact sales,” no asterisks.
TL;DR — Tracelet is a fully open-source (Apache 2.0) background geolocation plugin for Flutter. It does the battery-friendly, runs-when-the-app-is-dead, survives-a-reboot stuff that usually costs you a license fee. This month it crossed 7,200 downloads in 30 days. Here’s what’s inside — and why some of these features genuinely don’t exist anywhere else for free.

First, a confession
Background location on mobile is the dark souls of app development.
Getting the user’s location once? Easy. Two lines.
Keeping GPS alive when the app is backgrounded, the screen is off, the user force-quit it three hours ago, and the phone rebooted overnight — while not nuking the battery to 0% by lunch? That’s where dreams go to die.
Android kills background apps for sport. Samsung, Xiaomi, OnePlus, and Huawei each bolt on their own battery-optimization gremlins. iOS suspends your app within seconds of backgrounding and gives you a stern look if you ask for too much.
The “industry standard” solution to all this has historically been a brilliant but $500-per-app commercial plugin. Which is totally fair — that’s hard-earned engineering. But “fair” doesn’t help the indie dev shipping their first side project on a budget of vibes and instant noodles.
So I built Tracelet. Open source. Apache 2.0. Written from scratch on public platform APIs. Every line auditable, every line yours.
And then a funny thing happened: 7.2k downloads in 30 days. People apparently like free.
Let me show you the good parts.
The headline trick: it tracks by not tracking
Most location plugins poll GPS on a timer. GPS is the single most expensive thing your phone does that isn’t gaming. Polling it constantly is why “tracking apps” became synonymous with “battery murderers.”
Tracelet flips the model. It uses the accelerometer, gyroscope, and activity recognition to figure out whether you’re actually moving. Sitting still at your desk? GPS goes to sleep. Start walking, driving, or cycling? GPS wakes up instantly.
import 'package:tracelet/tracelet.dart' as tl;
tl.Tracelet.onLocation((tl.Location location) {
print('[location] $location');
});
tl.Tracelet.onMotionChange((tl.Location location) {
print('[motion] isMoving: ${location.isMoving}');
});
final state = await tl.Tracelet.ready(tl.Config.balanced().copyWith(
geo: tl.GeoConfig(
desiredAccuracy: tl.DesiredAccuracy.high,
distanceFilter: 10.0,
),
app: tl.AppConfig(stopOnTerminate: false, startOnBoot: true),
));
if (!state.enabled) {
await tl.Tracelet.start();
}
That’s the whole “hello world.” Three streams, one config, one start(). The battery savings come for free because the device only burns GPS when there's actually something worth recording.

The built-in live map example: speed-colored route trail, geofence visualization, and real-time status overlay.
No-permission mode bonus: Don’t want to ask for Activity Recognition? Set
disableMotionActivityUpdates: trueand Tracelet falls back to the raw hardware accelerometer + significant-motion sensor. Basic stationary↔moving detection, zero extra permissions required.
Same engine, same API, identical behavior on iOS and Android — moving vs. stationary detection in real time.
The features that genuinely don’t exist for free anywhere else
Plenty of plugins do “tracking.” Here’s the stuff that made people actually switch.
🔋 1. A Battery Budget Engine (yes, you literally set a target)
This is my favorite, because it’s slightly absurd in the best way. You tell Tracelet how much battery per hour you’re willing to spend:
Config(
battery: BatteryConfig(batteryBudgetPerHour: 2.0), // 2% per hour. That's it.
);
Tracelet.onBudgetAdjustment((adjustment) {
print('Engine retuned distanceFilter -> ${adjustment.distanceFilter}');
});
A feedback control loop then continuously retunes distanceFilter, desiredAccuracy, and the sampling interval to stay inside that budget. It's basically cruise control for battery drain. I have not seen this shipped for free anywhere.
🌍 2. Unlimited geofences (despite the OS saying “no”)
iOS caps you at 20 monitored regions. Android at 100. These are hard platform limits — everyone hits the same wall.
Tracelet uses proximity-based auto-load/unload backed by an R-tree spatial index (O(log n), sub-millisecond lookups on 10,000+ geofences). Only the closest regions get registered with the OS at any moment; the rest swap in as you move. The net effect: monitor thousands of geofences while the OS only ever sees a handful.
Plus polygon geofences — arbitrary vertices with ray-casting point-in-polygon checks, not just circles.
await Tracelet.addGeofence(Geofence(
identifier: 'downtown',
vertices: [LatLng(40.71, -74.01), LatLng(40.72, -74.00), /* ... */],
notifyOnEntry: true,
notifyOnExit: true,
));
💥 3. On-device crash & fall detection (with a real “I’m OK” countdown)
This one feels like sci-fi for an open-source plugin. Tracelet does corroborated impact detection — a big jolt alone is never enough; it has to happen while you’re actually moving fast. It even looks for extra real-world clues (the phone going weightless, sudden stillness, a sharp speed drop, an air-pressure “pop” from an airbag) to raise its confidence. When it fires, your user gets a cancel-countdown before it escalates to your SOS flow.
await Tracelet.ready(Config(
impact: ImpactConfig(
enableCrashDetection: true,
confirmWindowMs: 15000, // 15s for the user to say "I'm fine"
),
));
Tracelet.onImpact((event) async {
if (event.isPotential) {
// Show a big "Are you OK?" countdown screen.
await Tracelet.cancelImpact(event.id); // user tapped "I'm fine"
// await Tracelet.confirmImpact(event.id); // user tapped "Get help now"
} else {
// Confirmed crash/fall — the user didn't cancel in time. Start your SOS flow.
}
});
The killer detail: the countdown is process-death safe. A violent crash often ends with the OS killing your app — so Tracelet persists the pending event to disk and arms an exact AlarmManager alarm (Android) or local notification (iOS). If your app got killed, the confirmed event re-fires from a fresh process so your SOS flow still runs. That's the kind of thing that separates a demo from a product.
🤖 4. An actual trained AI crash model (commercially clear)
For the serious telematics/insurance crowd: the default rule engine (g-threshold + speed corroboration) works out of the box, but you can opt into a trained ML model that gates crashes on a learned probability instead of a dumb threshold. It’s:
- Trained on a CC0 / public-domain dataset → commercial-use OK
- Downloaded on demand (never bloating your app binary)
- AES-256-GCM encrypted at rest
- Backed by an automatic rule-engine fallback if the model can’t be activated (offline, bad key) — your app just keeps working
- Auto-updating when a new version ships
await Tracelet.ready(Config(
impact: ImpactConfig(
enableCrashDetection: true,
crashModelUnlockUrl: 'https://unlock.ikolvi.com/unlock',
crashModelLicenseKey: '<your license key>',
crashModelThreshold: 0.5074, // tuned probability from training
),
));
All of it runs on-device. No cloud round-trips, no per-API-call billing.
🚗 5. Driving telematics (harsh braking, speeding, cornering)
Harsh braking / acceleration / cornering / speeding detection straight from the GPS + accelerometer stream, each with a 0–1 severity score you can use to build trip-scoring or safe-driver features. 100% on-device, and it even works on Web.
await Tracelet.ready(Config(
telematics: TelematicsConfig(
enableDrivingEvents: true,
speedLimitKmh: 50,
),
));
Tracelet.onDrivingEvent((event) {
print('${event.kind} — severity ${event.severity}');
// e.g. harsh_braking — severity 0.82
});
Need to test your SOS screen without crashing an actual car? simulateTelematicsEvent(...) injects mock events right into the native engine, and getTelematicsEvents(50) pulls the persisted history from SQLite.
🚶 6. Transport-mode classifier
Fuses accelerometer + GPS to tell you whether the user is still, walking, running, cycling, or in a vehicle — with hysteresis so it doesn't flicker.
await Tracelet.ready(Config(
classifier: ClassifierConfig(enableFusedClassifier: true),
));
Tracelet.onModeChange((event) {
print('Now: ${event.mode} (confidence ${event.confidence})');
});
🧭 7. Dead reckoning (navigation when GPS dies)
Drive into a tunnel and most apps just… freeze the dot. Tracelet falls back to inertial navigation (accelerometer + gyroscope + compass) when GPS is lost, then gracefully auto-stops before IMU drift turns your position into fan fiction.
🛰️ 8. A Kalman filter for buttery-smooth tracks
GPS jitter makes your route trail look like a toddler’s crayon drawing. Flip on an Extended Kalman Filter and watch it smooth into a clean line:
GeoConfig(filter: LocationFilter(useKalmanFilter: true));
The “boring but you’ll desperately need it” tier
These don’t make flashy screenshots, but they’re the difference between a demo and a production app:
- SQLite persistence — every location stored locally, queryable, with retention limits (
maxDaysToPersist,maxRecordsToPersist). Works fully offline. Each record even captures the battery level + charging state at the moment it was recorded, so your backend can visualize battery drain across a route. - Headless execution — run Dart code in response to background events even when the Flutter UI isn’t running.
- Start on boot — resume tracking automatically after a reboot.
- Adaptive sampling — auto-adjusts
distanceFilterbased on activity, speed, and battery level. - Trip detection — automatic start/stop events with distance, duration, and full waypoint list via
onTrip(). - Route context — tag every location with business metadata (
taskId,driverId, custom keys) viasetRouteContext(), so your backend knows which delivery a coordinate belongs to.
Tracelet Sync: the network engine that refuses to lose your data
Location tracking is useless if the data never reaches your server — and mobile connections drop constantly (elevators, tunnels, Wi-Fi↔cellular handoffs). As of 3.2.0 this lives in its own module, **tracelet_sync** — an offline-first, battery-aware HTTP engine that delivers data without ever waking your UI.
import 'package:tracelet_sync/tracelet_sync.dart';
await TraceletSync.ready(SyncConfig(
url: 'https://your-api.com/locations',
method: 'POST',
autoSyncThreshold: 10, // sync once 10 locations are queued
autoSyncDelay: 10000, // debounce 10s so the radio settles
batchSync: true, // send as one JSON array
maxBatchSize: 250, // up to 250 locations per request
headers: {'Authorization': 'Bearer YOUR_TOKEN'},
));
What it handles for you, automatically:
- Offline queuing — no signal? Locations pile up safely in SQLite instead of failing HTTP requests and burning battery hunting for a tower.
- Batch + debounce — regains signal with 500 queued points? It waits, then bundles them into a couple of fat batches instead of 500 panic-requests.
- Delta encoding — a compression codec that shrinks payloads 60–80% by sending the first point in full and only the deltas after. Your bandwidth bill says thank you.
- Wi-Fi-only mode —
disableAutoSyncOnCellular: truekeeps roaming users' data plans intact; it flushes the moment they hit Wi-Fi. - 401-aware headless retry — token expired while the phone’s in a pocket? On an HTTP 401 it fires a headless Dart callback to refresh your JWT, then retries — even if the user force-quit the app.
- Exponential backoff — server down? It backs off (1s → 2s → 4s, capped) and after
maxRetriesleaves the data safely in SQLite for tomorrow. - Custom body builder —
setSyncBodyBuilder()lets you reshape the payload into whatever schema your legacy backend demands (also runs headlessly).
The enterprise stuff (also free, because why not)
- SSL certificate pinning — pin PEM certs or SHA-256 fingerprints to kill MITM attacks. Validated natively on both platforms.
- At-rest database encryption — AES-256 via the Rust core, keys in Android Keystore / iOS Secure Enclave. (Bonus: as of 3.2.0 this dropped the SQLCipher dependency and shaved ~16MB off the APK.)
- Device attestation — Google Play Integrity + iOS App Attest for server-side verification.
- GDPR/CCPA compliance reports —
generateComplianceReport()spits out a structured data-processing inventory (retention, encryption status, audit trail, permissions) as JSON or Markdown. Your legal team will weep with joy. - Carbon footprint estimator — per-trip CO₂ using EU EEA 2024 mode factors, because some of us are building climate apps.
The part where I admit it’s also nice to debug
Background location bugs are notoriously “works on my machine, dies on a Xiaomi in Brazil.” So there’s Tracelet Doctor — a drop-in diagnostic overlay:
TraceletDoctor.show(context);
It visualizes live tracking state, active sensors, the SQLite queue size, and OEM battery-optimization gotchas — with actionable fixes. One tap bundles health + config + logs into a paste-ready bug report (secrets redacted). Filing an issue has never been less painful.
There’s also a single-call getHealth() for building your own monitoring dashboards with zero boilerplate.
Tracelet vs. flutter_background_geolocation: the honest comparison
FBG is genuinely excellent software and deserves respect — it pioneered a lot of this. But here’s the free-vs-paid reality for a production app:
[embed]
Not a knock on FBG — it’s mature and battle-tested. But if you want the extras and the full source, Tracelet is a compelling free option.
Coming from flutter_background_geolocation? It's a 3-step swap
The API is 1:1 compatible on purpose. Migrating is genuinely a find-and-replace.
# Before
dependencies:
flutter_background_geolocation: ^5.x.x
# After
dependencies:
tracelet: # latest from pub.dev
tracelet_sync: # latest from pub.dev
// Before
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
bg.BackgroundGeolocation.ready(bg.Config(...));
// After
import 'package:tracelet/tracelet.dart' as tl;
tl.Tracelet.ready(tl.Config.balanced().copyWith(...));
That’s it. Every method, event, and callback maps across. Full guide: Migrating from FBG.
Under the hood (for the architecture nerds)
Tracelet is a federated plugin with a shared Rust core powering the heavy lifting (encryption, delta encoding, geofence math, Kalman filtering) identically across platforms:
[embed]
Tracking and syncing are deliberately decoupled — you only ship the native code and permissions you actually use. Want zero-backend syncing? Drop in tracelet_supabase or tracelet_firebase and your locations land in your database with no API to write.
Dart↔native is fully type-safe via Pigeon — no stringly-typed MethodChannel roulette. And the engines ship as standalone native SDKs too (Maven Central for Android, CocoaPods/SPM for iOS) if you want them without Flutter.
Why I’m giving this away
Honestly? Because background geolocation shouldn’t be a paywalled dark art. The indie dev building a delivery app, the student shipping a fitness tracker, the startup with no budget for a per-app license — they deserve production-grade tooling too.
7,200 downloads in 30 days tells me a lot of you agree.
It’s Apache 2.0. Fork it, audit it, ship it, sell your app built on it. No strings.
If it saves you a weekend (or a $500 line item), a ⭐ on GitHub or a coffee keeps me motivated to keep up with every new OS curveball Google and Apple throw at us.
🔗 Links
- 📦 pub.dev: pub.dev/packages/tracelet
- 📚 Docs: tracelet.ikolvi.com
- 💬 Discord: Join the community
- ⚡ Quick Start (2 min): tracelet.ikolvi.com/en/quick-start
Now go build something that knows where it’s going. 🛰️
Tags: Flutter, Dart, Mobile Development, Geolocation, Open Source
메타데이터
- post_id
- eb5fd00fb00d
- slug
- i-open-sourced-a-background-geolocation-plugin-for-flutter-eb5fd00fb00d
- url
- https://medium.com/@kiranbjm/i-open-sourced-a-background-geolocation-plugin-for-flutter-eb5fd00fb00d
- canonical_url
- https://medium.com/@kiranbjm/i-open-sourced-a-background-geolocation-plugin-for-flutter-eb5fd00fb00d
- author_url
- https://medium.com/@kiranbjm
- status
- ok
- fetched_at
- 2026-06-23 03:48:11