beekon_flutter 0.0.5 copy "beekon_flutter: ^0.0.5" to clipboard
beekon_flutter: ^0.0.5 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'),
          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()}'),
                  ),
                ),
            ],
          ),
        ),
      ],
    );
  }
}
1
likes
0
points
427
downloads

Publisher

verified publisherwayq.in

Weekly Downloads

Flutter plugin for the Beekon location SDK (Android + iOS).

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, meta, plugin_platform_interface

More

Packages that depend on beekon_flutter

Packages that implement beekon_flutter