beekon_flutter 0.0.3
beekon_flutter: ^0.0.3 copied to clipboard
Flutter plugin for the Beekon location SDK (Android + iOS).
example/lib/main.dart
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>[];
String? _error;
@override
void initState() {
super.initState();
_bootstrap();
Beekon.instance.locations.listen((Location p) {
setState(() {
_liveFeed.insert(0, p);
if (_liveFeed.length > 100) _liveFeed.removeLast();
});
});
}
Future<void> _bootstrap() async {
try {
await Beekon.instance.configure(
BeekonConfig(
intervalSeconds: 10,
distanceMeters: 30,
androidNotification: const AndroidNotification(
title: 'Beekon',
text: 'Tracking your location',
// mipmap/ic_launcher always exists in a flutter create scaffold.
smallIcon: 'ic_launcher',
),
),
);
} catch (e) {
setState(() => _error = e.toString());
}
}
Future<void> _toggle(BeekonState current) async {
try {
if (current is Tracking) {
await Beekon.instance.stop();
} else {
await Beekon.instance.start();
}
} catch (e) {
setState(() => _error = e.toString());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Beekon Flutter Example'),
actions: <Widget>[
IconButton(
tooltip: 'History',
icon: const Icon(Icons.history),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext _) => const HistoryPage(),
),
),
),
],
),
body: StreamBuilder<BeekonState>(
stream: Beekon.instance.state,
initialData: const Idle(),
builder: (BuildContext context, AsyncSnapshot<BeekonState> snap) {
final BeekonState state = snap.data ?? const Idle();
return Column(
children: <Widget>[
_StatusCard(state: state, error: _error),
Padding(
padding: const EdgeInsets.all(16),
child: FilledButton.icon(
icon: Icon(state is Tracking ? Icons.stop : Icons.play_arrow),
label: Text(state is Tracking ? 'Stop' : 'Start'),
onPressed: () => _toggle(state),
),
),
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const Text('Live locations'),
Text('${_liveFeed.length} buffered',
style: Theme.of(context).textTheme.bodySmall),
],
),
),
Expanded(
child: ListView.builder(
itemCount: _liveFeed.length,
itemBuilder: (BuildContext context, int i) {
final Location p = _liveFeed[i];
return _LocationTile(p: p);
},
),
),
],
);
},
),
);
}
}
class _StatusCard extends StatelessWidget {
const _StatusCard({required this.state, required this.error});
final BeekonState state;
final String? error;
@override
Widget build(BuildContext context) {
final ThemeData t = Theme.of(context);
final String label = switch (state) {
Idle() => 'Idle',
Tracking() => 'Tracking',
Stopped(reason: final StopReason r) => 'Stopped (${r.name})',
};
return 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),
const SizedBox(height: 4),
Text(label, style: t.textTheme.headlineSmall),
if (error != null) ...<Widget>[
const SizedBox(height: 8),
Text(error!,
style: t.textTheme.bodySmall?.copyWith(
color: t.colorScheme.error)),
],
],
),
),
);
}
}
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.lat.toStringAsFixed(5)}, ${p.lng.toStringAsFixed(5)}'),
subtitle: Text(
'±${p.accuracy?.toStringAsFixed(1) ?? '—'} m • '
'${p.speed?.toStringAsFixed(1) ?? '—'} m/s • '
'${p.timestamp.toLocal()}',
),
);
}
}
class HistoryPage extends StatefulWidget {
const HistoryPage({super.key});
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
late DateTime _from;
late DateTime _to;
Future<List<Location>>? _future;
@override
void initState() {
super.initState();
_to = DateTime.now();
_from = _to.subtract(const Duration(hours: 24));
}
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) {
setState(() {
_from = range.start;
_to = range.end;
_future = Beekon.instance.history(from: _from, to: _to);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('History'),
actions: <Widget>[
IconButton(
tooltip: 'Pick date range',
icon: const Icon(Icons.date_range),
onPressed: _pickRange,
),
],
),
body: FutureBuilder<List<Location>>(
future: _future ??= Beekon.instance.history(from: _from, to: _to),
builder:
(BuildContext context, AsyncSnapshot<List<Location>> snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return Center(child: Text('Error: ${snap.error}'));
}
final List<Location> rows = snap.data ?? <Location>[];
if (rows.isEmpty) {
return const Center(child: Text('No history in this range.'));
}
return ListView.builder(
itemCount: rows.length,
itemBuilder: (BuildContext context, int i) =>
_LocationTile(p: rows[i]),
);
},
),
);
}
}