Lifecycle & states
Beekon goes through exactly three states. They’re identical on every platform — only the casing follows the host language’s idiom (Tracking on Kotlin, tracking on Swift).
Idle → Tracking → Stopped(reason)You observe state via the state stream — it’s hot and replay-1, so every consumer sees the latest value the moment they subscribe.
The states
Section titled “The states”| State | Meaning | Live emissions? |
|---|---|---|
Idle | Initial state. configure may or may not have happened; start has not. | No |
Tracking | Live updates flowing from the OS provider through the gate. | Yes |
Stopped(reason) | Tracking ended. The reason indicates whether it was user-initiated or forced. Re-enter via start(). | No |
There’s no Starting window — start() is suspending and is the transition into Tracking. There’s no Paused state either — preconditions that fail at runtime (permission revoked, location services disabled, OS-killed foreground service on Android) surface as a terminal Stopped(reason) and the host app calls start() again once the condition recovers.
Stop reasons
Section titled “Stop reasons”| Reason | When it fires |
|---|---|
User | The host app called Beekon.stop(). |
PermissionDenied | Location permission was revoked or never granted. |
LocationServicesDisabled | System-wide location services were turned off (iOS), or services off / Google Play Services unavailable (Android). |
System | Android only. The OS terminated the foreground service (memory pressure, force-stop). iOS never observes this case. |
Stop reasons match across platforms (.permissionDenied on Swift, PermissionDenied on Kotlin, permissionDenied on Dart and TypeScript).
Observing state
Section titled “Observing state”// state is a StateFlow — replay-1, hotval state by Beekon.state.collectAsStateWithLifecycle()
when (val s = state) { is BeekonState.Idle -> Text("Not started") is BeekonState.Tracking -> Text("Live") is BeekonState.Stopped -> Text("Stopped: ${s.reason}")}Task { for await state in await Beekon.shared.state { switch state { case .idle: Text("Not started") case .tracking: Text("Live") case .stopped(let reason): Text("Stopped: \(reason)") } }}StreamBuilder<BeekonState>( stream: Beekon.instance.state, initialData: const Idle(), builder: (ctx, snap) => switch (snap.data!) { Idle() => const Text('Not started'), Tracking() => const Text('Live'), Stopped(:final reason) => Text('Stopped: $reason'), },);const unsubscribe = Beekon.onState((s) => { switch (s.kind) { case 'idle': return setLabel('Not started'); case 'tracking': return setLabel('Live'); case 'stopped': return setLabel(`Stopped: ${s.reason}`); }});Transitions
Section titled “Transitions” start() stop() / OS-revocation / system killIdle ────────> Tracking ──────────────────────────────> Stopped(reason) ▲ │ └────────────── start() ─────────────────────┘Idle → Tracking:start()succeeds. The first admitted fix can arrive immediately.Tracking → Stopped(User): the host calledstop().Tracking → Stopped(PermissionDenied): location authorization was revoked while tracking.Tracking → Stopped(LocationServicesDisabled): system services were turned off, or — on Android — Google Play Services became unavailable.Tracking → Stopped(System): Android only — the foreground service was killed by the OS (memory pressure, force-stop).Stopped → Tracking: requires a freshstart()call. Beekon does not auto-resume when a permission or services condition recovers.