beekon_flutter 0.0.6
beekon_flutter: ^0.0.6 copied to clipboard
Flutter plugin for the Beekon location SDK (Android + iOS).
example/lib/main.dart
import 'dart:async';
import 'package:beekon_flutter/beekon_flutter.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const BeekonExampleApp());
}
class BeekonExampleApp extends StatelessWidget {
const BeekonExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Beekon Flutter Example',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.amber),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final List<Location> _liveFeed = <Location>[];
final List<GeofenceEvent> _geofenceLog = <GeofenceEvent>[];
BeekonState _state = const Idle();
SyncStatus _syncStatus = const SyncIdle();
String? _error;
final List<StreamSubscription<Object?>> _subs = <StreamSubscription<Object?>>[];
@override
void initState() {
super.initState();
_subscribe();
_bootstrap();
}
void _subscribe() {
_subs.add(Beekon.instance.state.listen((BeekonState s) {
setState(() => _state = s);
}));
_subs.add(Beekon.instance.locations.listen((Location p) {
setState(() {
_liveFeed.insert(0, p);
if (_liveFeed.length > 100) _liveFeed.removeLast();
});
}));
_subs.add(Beekon.instance.syncStatus.listen((SyncStatus s) {
setState(() => _syncStatus = s);
}));
_subs.add(Beekon.instance.geofenceEvents.listen((GeofenceEvent e) {
setState(() {
_geofenceLog.insert(0, e);
if (_geofenceLog.length > 50) _geofenceLog.removeLast();
});
}));
}
Future<void> _bootstrap() async {
try {
await Beekon.instance.configure(
const BeekonConfig(
minTimeBetweenLocationsSeconds: 10,
minDistanceBetweenLocationsMeters: 30,
accuracyMode: AccuracyMode.balanced,
whenStationary: StationaryMode.pauseWithCheckIns,
detectActivity: true,
// Add a SyncConfig here to enable background upload, e.g.:
// sync: SyncConfig(url: 'https://your.endpoint/ingest'),
// For native token refresh, set sync.auth and observe
// Beekon.instance.authChanges to persist rotated tokens, e.g.:
// sync: SyncConfig(
// url: 'https://your.endpoint/ingest',
// auth: AuthConfig(
// accessToken: '<short-lived-jwt>',
// refreshToken: '<refresh-token>',
// refreshUrl: 'https://your.endpoint/auth/refresh',
// ),
// ),
notification: NotificationConfig(
title: 'Beekon',
text: 'Tracking your location',
),
),
);
await Beekon.instance.setExtras(<String, String>{'user_id': 'demo'});
// Resume a session that survived process death (no-op if user stopped).
await Beekon.instance.resumeIfNeeded();
} on BeekonException catch (e) {
setState(() => _error = e.toString());
}
}
@override
void dispose() {
for (final StreamSubscription<Object?> s in _subs) {
s.cancel();
}
super.dispose();
}
Future<void> _toggle() async {
try {
if (_state is Tracking) {
await Beekon.instance.stop();
} else {
await Beekon.instance.start();
}
} on BeekonException catch (e) {
setState(() => _error = e.toString());
}
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Beekon Flutter Example'),
bottom: const TabBar(
tabs: <Widget>[
Tab(text: 'Live', icon: Icon(Icons.my_location)),
Tab(text: 'History', icon: Icon(Icons.history)),
Tab(text: 'Geofences', icon: Icon(Icons.fence)),
],
),
),
body: TabBarView(
children: <Widget>[
_LiveTab(
state: _state,
syncStatus: _syncStatus,
error: _error,
feed: _liveFeed,
onToggle: _toggle,
),
const HistoryTab(),
GeofenceTab(
log: _geofenceLog,
lastLocation: _liveFeed.isNotEmpty ? _liveFeed.first : null,
),
],
),
),
);
}
}
class _LiveTab extends StatelessWidget {
const _LiveTab({
required this.state,
required this.syncStatus,
required this.error,
required this.feed,
required this.onToggle,
});
final BeekonState state;
final SyncStatus syncStatus;
final String? error;
final List<Location> feed;
final Future<void> Function() onToggle;
String get _stateLabel => switch (state) {
Idle() => 'Idle',
Tracking() => 'Tracking',
Stopped(reason: final StopReason r) => 'Stopped (${r.name})',
};
String get _syncLabel => switch (syncStatus) {
SyncIdle() => 'Idle',
SyncPending() => 'Pending',
SyncFailed(reason: final SyncFailure r) => 'Failed (${r.name})',
};
@override
Widget build(BuildContext context) {
final ThemeData t = Theme.of(context);
return Column(
children: <Widget>[
Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('State', style: t.textTheme.bodySmall),
Text(_stateLabel, style: t.textTheme.headlineSmall),
const SizedBox(height: 8),
Text('Sync: $_syncLabel', style: t.textTheme.bodyMedium),
if (error != null) ...<Widget>[
const SizedBox(height: 8),
Text(
error!,
style: t.textTheme.bodySmall
?.copyWith(color: t.colorScheme.error),
),
],
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FilledButton.icon(
icon: Icon(state is Tracking ? Icons.stop : Icons.play_arrow),
label: Text(state is Tracking ? 'Stop' : 'Start'),
onPressed: onToggle,
),
),
const Divider(),
Expanded(
child: feed.isEmpty
? const Center(child: Text('No live fixes yet.'))
: ListView.builder(
itemCount: feed.length,
itemBuilder: (BuildContext context, int i) =>
_LocationTile(p: feed[i]),
),
),
],
);
}
}
class _LocationTile extends StatelessWidget {
const _LocationTile({required this.p});
final Location p;
@override
Widget build(BuildContext context) {
return ListTile(
dense: true,
leading: const Icon(Icons.place_outlined, size: 20),
title: Text(
'${p.latitude.toStringAsFixed(5)}, ${p.longitude.toStringAsFixed(5)}',
),
subtitle: Text(
'±${p.accuracy?.toStringAsFixed(1) ?? '—'} m • '
'${p.trigger.name} • ${p.motion.name}\n'
'${p.timestamp.toLocal()}',
),
isThreeLine: true,
);
}
}
class HistoryTab extends StatefulWidget {
const HistoryTab({super.key});
@override
State<HistoryTab> createState() => _HistoryTabState();
}
class _HistoryTabState extends State<HistoryTab> {
late DateTime _from;
late DateTime _to;
List<Location> _rows = <Location>[];
int? _pending;
String? _message;
bool _loading = false;
@override
void initState() {
super.initState();
_to = DateTime.now();
_from = _to.subtract(const Duration(hours: 24));
_reload();
}
Future<void> _reload() async {
setState(() => _loading = true);
try {
final List<Location> rows =
await Beekon.instance.getLocations(from: _from, to: _to);
final int pending = await Beekon.instance.pendingUploadCount();
setState(() {
_rows = rows;
_pending = pending;
_message = null;
});
} on BeekonException catch (e) {
setState(() => _message = e.toString());
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _pickRange() async {
final DateTimeRange? range = await showDateRangePicker(
context: context,
firstDate: DateTime(2025),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(start: _from, end: _to),
);
if (range != null) {
_from = range.start;
_to = range.end.add(const Duration(hours: 23, minutes: 59));
await _reload();
}
}
Future<void> _deleteAll() async {
try {
final int removed = await Beekon.instance.deleteLocations();
setState(() => _message = 'Deleted $removed rows');
await _reload();
} on BeekonException catch (e) {
setState(() => _message = e.toString());
}
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: <Widget>[
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.date_range),
label: const Text('Range'),
onPressed: _pickRange,
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.delete_outline),
label: const Text('Delete all'),
onPressed: _deleteAll,
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('${_rows.length} rows'),
Text('Pending upload: ${_pending ?? '—'}'),
],
),
),
if (_message != null)
Padding(
padding: const EdgeInsets.all(8),
child: Text(_message!),
),
const Divider(),
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _rows.isEmpty
? const Center(child: Text('No history in this range.'))
: ListView.builder(
itemCount: _rows.length,
itemBuilder: (BuildContext context, int i) =>
_LocationTile(p: _rows[i]),
),
),
],
);
}
}
class GeofenceTab extends StatefulWidget {
const GeofenceTab({required this.log, required this.lastLocation, super.key});
final List<GeofenceEvent> log;
final Location? lastLocation;
@override
State<GeofenceTab> createState() => _GeofenceTabState();
}
class _GeofenceTabState extends State<GeofenceTab> {
List<BeekonGeofence> _geofences = <BeekonGeofence>[];
String? _message;
int _counter = 0;
@override
void initState() {
super.initState();
_reload();
}
Future<void> _reload() async {
try {
final List<BeekonGeofence> list = await Beekon.instance.listGeofences();
setState(() => _geofences = list);
} on BeekonException catch (e) {
setState(() => _message = e.toString());
}
}
Future<void> _addSample() async {
// Anchor on the last live fix if available, else a default coordinate.
final double lat = widget.lastLocation?.latitude ?? 37.7749;
final double lng = widget.lastLocation?.longitude ?? -122.4194;
try {
await Beekon.instance.addGeofences(<BeekonGeofence>[
BeekonGeofence(
id: 'sample-${_counter++}',
latitude: lat,
longitude: lng,
radiusMeters: 150,
),
]);
setState(() => _message = null);
await _reload();
} on InvalidGeofence catch (e) {
setState(() => _message = e.reason);
} on BeekonException catch (e) {
setState(() => _message = e.toString());
}
}
Future<void> _remove(String id) async {
await Beekon.instance.removeGeofences(<String>[id]);
await _reload();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12),
child: FilledButton.icon(
icon: const Icon(Icons.add_location_alt),
label: const Text('Add sample geofence'),
onPressed: _addSample,
),
),
if (_message != null)
Padding(padding: const EdgeInsets.all(8), child: Text(_message!)),
Expanded(
child: ListView(
children: <Widget>[
const ListTile(dense: true, title: Text('Registered')),
if (_geofences.isEmpty)
const ListTile(title: Text('None'))
else
..._geofences.map(
(BeekonGeofence g) => ListTile(
dense: true,
leading: const Icon(Icons.fence_outlined, size: 20),
title: Text(g.id),
subtitle: Text(
'${g.latitude.toStringAsFixed(4)}, '
'${g.longitude.toStringAsFixed(4)} r=${g.radiusMeters}m',
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _remove(g.id),
),
),
),
const Divider(),
const ListTile(dense: true, title: Text('Event log')),
if (widget.log.isEmpty)
const ListTile(title: Text('No crossings yet'))
else
...widget.log.map(
(GeofenceEvent e) => ListTile(
dense: true,
leading: Icon(
e.type == Transition.enter ? Icons.login : Icons.logout,
size: 20,
),
title: Text('${e.type.name} — ${e.geofenceId}'),
subtitle: Text('${e.timestamp.toLocal()}'),
),
),
],
),
),
],
);
}
}