Positions
A Position is exactly seven fields:
lat, lng, accuracy, speed, bearing, altitude, timestampThe fields are identical on every platform. Numeric widths differ — Android uses Float for accuracy/speed/bearing (matching android.location.Location); iOS uses Double (matching CLLocation). Convert at the bridge layer if you need uniform widths.
| Field | Unit | Source |
|---|---|---|
lat, lng | degrees (WGS-84) | FusedLocationProviderClient.getLastLocation() / CLLocation.coordinate |
accuracy | metres (horizontal, 1σ) | OS provider’s reported accuracy |
speed | metres/second | OS provider — already smoothed/fused |
bearing | degrees (0–360, true north) | OS provider — direction of travel, not device heading |
altitude | metres above WGS-84 ellipsoid | OS provider |
timestamp | wall-clock | OS provider’s fix time, not the time Beekon received it |
Raw passthrough
Section titled “Raw passthrough”This is non-negotiable in v1: Beekon does not modify positions. No Kalman filter, no outlier rejection, no speed clamp, no minimum-accuracy filter. Whatever the OS provider produces is what you get on the stream and in history.
A consequence: adjacent positions can disagree dramatically (urban canyon, tunnel, cold start). That’s the OS doing its honest best — surface it to users via the accuracy field rather than hiding it.
Reading positions
Section titled “Reading positions”The positions stream is hot — it emits the latest gated position to all subscribers. On Android the buffer is replay-1, DROP_OLDEST, capacity 64 (so a slow consumer drops old emissions but never blocks the producer). iOS uses AsyncStream with equivalent semantics.
// SharedFlow<Position> — replay-1, DROP_OLDEST, buffer 64LaunchedEffect(Unit) { Beekon.positions.collect { p -> Log.d("beekon", "${p.lat}, ${p.lng} acc=${p.accuracy}m speed=${p.speed}m/s") }}Task { for await p in await Beekon.shared.positions { print("\(p.lat), \(p.lng) acc=\(p.accuracy)m speed=\(p.speed)m/s") }}Beekon.instance.positions.listen((Position p) { print('${p.lat}, ${p.lng} acc=${p.accuracy}m speed=${p.speed}m/s');});The Flutter stream only emits while the Flutter engine is alive — for points captured during process death, query history.
Filtering on top
Section titled “Filtering on top”The most common shape: drop low-accuracy points when rendering on a map.
Beekon.positions .filter { it.accuracy <= 25f } .collect { /* render */ }for await p in await Beekon.shared.positions where p.accuracy <= 25 { // render}Beekon.instance.positions .where((p) => p.accuracy <= 25) .listen((p) { /* render */ });The persistence layer still records the raw point — filtering happens in your stream consumer, not in storage.
Live stream vs history
Section titled “Live stream vs history”Two separate read paths:
positionsstream — real-time, replay-1. Drops emissions older than the last seen one. Only flows while the host process is alive (for Flutter: while the Flutter engine is alive).history(from, to)— durable. Reads the native database (Room/SQLite on Android, GRDB/SQLite on iOS). Survives process death and termination. See Persistence & history.
In practice: render the live stream while the user is on a tracking screen; query history when they open a “trips” view.