Skip to main content

advanced_geofence 0.1.2 copy "advanced_geofence: ^0.1.2" to clipboard
advanced_geofence: ^0.1.2 copied to clipboard

Pure Dart geofencing engine: enter, exit and dwell events for circle and polygon zones from any location stream, with noise filters and saveable state.

example/lib/main.dart

import 'dart:async';
import 'dart:isolate';
import 'dart:ui';

import 'package:advanced_background_locator/advanced_background_locator.dart';
import 'package:advanced_geofence/advanced_geofence.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';

import 'background.dart';
import 'geofence_store.dart';
import 'l10n/app_localizations.dart';

void main() => runApp(const GeofenceExampleApp());

const Color _brandColor = Color(0xFF0B3D91);
const Color _insideColor = Color(0xFF1B873F);

class GeofenceExampleApp extends StatelessWidget {
  const GeofenceExampleApp({super.key});

  ThemeData _theme(Brightness brightness) {
    final scheme = ColorScheme.fromSeed(seedColor: _brandColor, brightness: brightness);
    return ThemeData(
      colorScheme: scheme,
      useMaterial3: true,
      scaffoldBackgroundColor: scheme.surfaceContainerLowest,
      appBarTheme: AppBarTheme(
        backgroundColor: scheme.surfaceContainerLowest,
        surfaceTintColor: Colors.transparent,
        centerTitle: false,
      ),
      cardTheme: CardThemeData(
        elevation: 0,
        margin: EdgeInsets.zero,
        color: scheme.surface,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
          side: BorderSide(color: scheme.outlineVariant),
        ),
      ),
      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
          minimumSize: const Size.fromHeight(52),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
          textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
      debugShowCheckedModeBanner: false,
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      // Device languages other than Turkish fall back to English.
      localeResolutionCallback: (locale, supported) =>
          supported.firstWhere((l) => l.languageCode == locale?.languageCode, orElse: () => const Locale('en')),
      theme: _theme(Brightness.light),
      darkTheme: _theme(Brightness.dark),
      home: const GeofencePage(),
    );
  }
}

class GeofencePage extends StatefulWidget {
  const GeofencePage({super.key});

  @override
  State<GeofencePage> createState() => _GeofencePageState();
}

class _GeofencePageState extends State<GeofencePage> {
  final ReceivePort _port = ReceivePort();
  final GeofenceStore _store = GeofenceStore();

  bool _isRunning = false;
  bool _isBusy = false;
  GeoPoint? _lastPoint;
  IgnoreReason? _lastIgnored;
  List<Geofence> _zones = [];
  GeofenceEngineState? _state;
  List<GeofenceEvent> _events = [];

  @override
  void initState() {
    super.initState();
    // Replace a mapping left over from a previous UI isolate.
    IsolateNameServer.removePortNameMapping(uiPortName);
    IsolateNameServer.registerPortWithName(_port.sendPort, uiPortName);
    _port.listen(_onBackgroundMessage);
    _init();
  }

  @override
  void dispose() {
    IsolateNameServer.removePortNameMapping(uiPortName);
    _port.close();
    super.dispose();
  }

  Future<void> _init() async {
    await AdvancedBackgroundLocator.initialize();
    final isRunning = await AdvancedBackgroundLocator.isServiceRunning();
    final zones = await _store.loadZones();
    final state = await _store.loadState();
    final events = await _store.loadEvents();
    if (!mounted) return;
    setState(() {
      _isRunning = isRunning;
      _zones = zones;
      _state = state;
      _events = events;
      _lastPoint = state?.lastPoint;
    });
  }

  /// Messages sent by [geofenceLocationCallback] from the background isolate.
  Future<void> _onBackgroundMessage(dynamic data) async {
    if (data is! Map) return;
    final location = data['location'] as Map?;
    final ignored = data['ignored'] as String?;
    final events = [
      for (final e in (data['events'] as List?) ?? const []) GeofenceEvent.fromJson((e as Map).cast<String, Object?>()),
    ];
    final state = await _store.loadState();
    if (!mounted) return;
    setState(() {
      if (location != null) _lastPoint = GeoPoint.fromJson(location.cast<String, Object?>());
      _lastIgnored = ignored == null ? null : IgnoreReason.values.byName(ignored);
      _state = state;
      _events = [...events.reversed, ..._events].take(GeofenceStore.maxEvents).toList();
    });
    if (events.isNotEmpty) await _updateNotification(events.last);
  }

  Future<void> _updateNotification(GeofenceEvent event) async {
    if (!mounted) return;
    final l10n = AppLocalizations.of(context);
    await AdvancedBackgroundLocator.updateNotificationText(
      title: _eventTitle(l10n, event),
      msg: _formatTime(context, event.time.toLocal()),
    );
  }

  Future<void> _onStart() async {
    setState(() => _isBusy = true);
    try {
      final granted = await _requestPermissions();
      if (!mounted) return;
      if (!granted) {
        _showMessage(AppLocalizations.of(context).permissionDenied);
        return;
      }
      await _startLocator();
      final isRunning = await AdvancedBackgroundLocator.isServiceRunning();
      if (!mounted) return;
      setState(() => _isRunning = isRunning);
    } on PlatformException catch (e) {
      _showMessage(e.message ?? e.code);
    } finally {
      if (mounted) setState(() => _isBusy = false);
    }
  }

  Future<void> _onStop() async {
    setState(() => _isBusy = true);
    await AdvancedBackgroundLocator.unRegisterLocationUpdate();
    final isRunning = await AdvancedBackgroundLocator.isServiceRunning();
    if (!mounted) return;
    setState(() {
      _isRunning = isRunning;
      _isBusy = false;
    });
  }

  /// Foreground location is required; background location ("Allow all the
  /// time") and notifications are requested but optional.
  Future<bool> _requestPermissions() async {
    if (!(await Permission.locationWhenInUse.request()).isGranted) return false;
    await Permission.notification.request();
    await Permission.locationAlways.request();
    return true;
  }

  Future<void> _startLocator() async {
    final l10n = AppLocalizations.of(context);
    await AdvancedBackgroundLocator.registerLocationUpdate(
      geofenceLocationCallback,
      initCallback: geofenceInitCallback,
      disposeCallback: geofenceDisposeCallback,
      iosSettings: const IOSSettings(accuracy: LocationAccuracy.NAVIGATION, distanceFilter: 5),
      androidSettings: AndroidSettings(
        accuracy: LocationAccuracy.NAVIGATION,
        interval: 5,
        distanceFilter: 5,
        client: LocationClient.google,
        androidNotificationSettings: AndroidNotificationSettings(
          notificationChannelName: l10n.notificationChannelName,
          notificationTitle: l10n.notificationTitle,
          notificationMsg: l10n.notificationMessage,
          notificationBigMsg: l10n.notificationBigMessage,
          notificationIconColor: _brandColor,
          notificationTapCallback: notificationTapCallback,
        ),
      ),
    );
  }

  Future<void> _addZone() async {
    final point = _lastPoint;
    if (point == null) return;
    final l10n = AppLocalizations.of(context);
    final result = await showModalBottomSheet<(String, double)>(
      context: context,
      isScrollControlled: true,
      showDragHandle: true,
      builder: (_) => _AddZoneSheet(defaultName: l10n.zoneNameDefault(_zones.length + 1)),
    );
    if (result == null) return;
    final zone = CircleGeofence(
      id: 'zone-${DateTime.now().millisecondsSinceEpoch}',
      name: result.$1,
      center: GeoPoint(point.latitude, point.longitude),
      radius: result.$2,
      dwellTime: const Duration(minutes: 1),
    );
    final zones = [..._zones, zone];
    // The background isolate notices the changed file on its next reading.
    await _store.saveZones(zones);
    if (!mounted) return;
    setState(() => _zones = zones);
  }

  Future<void> _deleteZone(Geofence zone) async {
    final zones = [
      for (final z in _zones)
        if (z.id != zone.id) z,
    ];
    await _store.saveZones(zones);
    if (!mounted) return;
    setState(() => _zones = zones);
  }

  Future<void> _clearEvents() async {
    await _store.clearEvents();
    if (!mounted) return;
    setState(() => _events = []);
  }

  void _showMessage(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = AppLocalizations.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(l10n.appTitle, style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700)),
            Text(
              l10n.appSubtitle,
              style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
            ),
          ],
        ),
        actions: [
          IconButton(
            tooltip: l10n.clearEvents,
            onPressed: _events.isEmpty ? null : _clearEvents,
            icon: const Icon(Icons.delete_sweep_outlined),
          ),
        ],
      ),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
          children: [
            _StatusCard(isRunning: _isRunning),
            const SizedBox(height: 16),
            _ActionButton(isRunning: _isRunning, isBusy: _isBusy, onStart: _onStart, onStop: _onStop),
            const SizedBox(height: 28),
            _SectionHeader(title: l10n.sectionPosition),
            const SizedBox(height: 8),
            _PositionCard(
              point: _lastPoint,
              ignored: _lastIgnored,
              isRunning: _isRunning,
              onAddZone: _isRunning && _lastPoint != null ? _addZone : null,
            ),
            const SizedBox(height: 28),
            _SectionHeader(title: l10n.sectionZones, trailing: _zones.isEmpty ? null : l10n.zoneCount(_zones.length)),
            const SizedBox(height: 8),
            _ZonesCard(zones: _zones, state: _state, point: _lastPoint, onDelete: _deleteZone),
            const SizedBox(height: 28),
            _SectionHeader(
              title: l10n.sectionEvents,
              trailing: _events.isEmpty ? null : l10n.eventCount(_events.length),
            ),
            const SizedBox(height: 8),
            _EventsCard(events: _events),
            const SizedBox(height: 20),
            Text(
              l10n.settingsSummary,
              textAlign: TextAlign.center,
              style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
            ),
          ],
        ),
      ),
    );
  }
}

String _formatTime(BuildContext context, DateTime t) =>
    DateFormat.Hms(Localizations.localeOf(context).toLanguageTag()).format(t);

String _formatNumber(BuildContext context, double value, int decimals) => NumberFormat.decimalPatternDigits(
  locale: Localizations.localeOf(context).toLanguageTag(),
  decimalDigits: decimals,
).format(value);

String _meters(BuildContext context, double value) =>
    AppLocalizations.of(context).unitMeters(_formatNumber(context, value, 0));

String _eventTitle(AppLocalizations l10n, GeofenceEvent event) {
  final name = event.geofence.name ?? event.geofenceId;
  return switch (event.type) {
    GeofenceEventType.enter => l10n.eventEnter(name),
    GeofenceEventType.exit => l10n.eventExit(name),
    GeofenceEventType.dwell => l10n.eventDwell(name),
  };
}

class _StatusCard extends StatelessWidget {
  const _StatusCard({required this.isRunning});

  final bool isRunning;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final scheme = theme.colorScheme;
    final l10n = AppLocalizations.of(context);
    final Color accent = isRunning ? _insideColor : scheme.outline;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Container(
              width: 48,
              height: 48,
              decoration: BoxDecoration(
                color: isRunning ? scheme.primaryContainer : scheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(12),
              ),
              child: Icon(
                isRunning ? Icons.radar : Icons.location_disabled_outlined,
                color: isRunning ? scheme.onPrimaryContainer : scheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(width: 14),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    isRunning ? l10n.statusActive : l10n.statusInactive,
                    style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 2),
                  Text(
                    isRunning ? l10n.statusActiveDescription : l10n.statusInactiveDescription,
                    style: theme.textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant),
                  ),
                ],
              ),
            ),
            const SizedBox(width: 8),
            _Badge(label: isRunning ? l10n.badgeLive : l10n.badgeIdle, color: accent),
          ],
        ),
      ),
    );
  }
}

class _Badge extends StatelessWidget {
  const _Badge({required this.label, required this.color});

  final String label;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
      decoration: BoxDecoration(color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(20)),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: 8,
            height: 8,
            decoration: BoxDecoration(color: color, shape: BoxShape.circle),
          ),
          const SizedBox(width: 6),
          Text(
            label,
            style: Theme.of(context).textTheme.labelMedium?.copyWith(color: color, fontWeight: FontWeight.w600),
          ),
        ],
      ),
    );
  }
}

class _ActionButton extends StatelessWidget {
  const _ActionButton({required this.isRunning, required this.isBusy, required this.onStart, required this.onStop});

  final bool isRunning;
  final bool isBusy;
  final VoidCallback onStart;
  final VoidCallback onStop;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final l10n = AppLocalizations.of(context);
    final Widget icon = isBusy
        ? SizedBox(
            width: 20,
            height: 20,
            child: CircularProgressIndicator(strokeWidth: 2, color: isRunning ? scheme.onError : scheme.onPrimary),
          )
        : Icon(isRunning ? Icons.stop_rounded : Icons.play_arrow_rounded);

    return FilledButton.icon(
      onPressed: isBusy ? null : (isRunning ? onStop : onStart),
      style: isRunning ? FilledButton.styleFrom(backgroundColor: scheme.error, foregroundColor: scheme.onError) : null,
      icon: icon,
      label: Text(isRunning ? l10n.stopMonitoring : l10n.startMonitoring),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  const _SectionHeader({required this.title, this.trailing});

  final String title;
  final String? trailing;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Row(
      children: [
        Expanded(
          child: Text(
            title.toUpperCase(),
            style: theme.textTheme.labelMedium?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
              fontWeight: FontWeight.w700,
              letterSpacing: 0.8,
            ),
          ),
        ),
        if (trailing != null)
          Text(trailing!, style: theme.textTheme.labelMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant)),
      ],
    );
  }
}

class _PositionCard extends StatelessWidget {
  const _PositionCard({required this.point, required this.ignored, required this.isRunning, required this.onAddZone});

  final GeoPoint? point;
  final IgnoreReason? ignored;
  final bool isRunning;
  final VoidCallback? onAddZone;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final scheme = theme.colorScheme;
    final l10n = AppLocalizations.of(context);
    final p = point;
    if (p == null) {
      return _EmptyCard(
        icon: Icons.satellite_alt_outlined,
        message: isRunning ? l10n.waitingForLocation : l10n.noLocationYet,
      );
    }

    // Coordinates keep the decimal point in every language so they can be copied as-is.
    final metrics = <(String, String)>[
      (l10n.metricLatitude, p.latitude.toStringAsFixed(5)),
      (l10n.metricLongitude, p.longitude.toStringAsFixed(5)),
      (l10n.metricAccuracy, p.accuracy == null ? '-' : '± ${_meters(context, p.accuracy!)}'),
      (l10n.metricTime, p.time == null ? '-' : _formatTime(context, p.time!.toLocal())),
    ];
    final reason = switch (ignored) {
      IgnoreReason.lowAccuracy => l10n.ignoredLowAccuracy,
      IgnoreReason.stale => l10n.ignoredStale,
      IgnoreReason.outOfOrder => l10n.ignoredOutOfOrder,
      IgnoreReason.invalid => l10n.ignoredInvalid,
      null => null,
    };

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            for (var row = 0; row < metrics.length; row += 2) ...[
              if (row > 0) const SizedBox(height: 16),
              Row(
                children: [
                  Expanded(
                    child: _Metric(label: metrics[row].$1, value: metrics[row].$2),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: _Metric(label: metrics[row + 1].$1, value: metrics[row + 1].$2),
                  ),
                ],
              ),
            ],
            if (reason != null) ...[
              const SizedBox(height: 12),
              Row(
                children: [
                  Icon(Icons.info_outline, size: 16, color: scheme.onSurfaceVariant),
                  const SizedBox(width: 6),
                  Expanded(
                    child: Text(
                      l10n.readingIgnored(reason),
                      style: theme.textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant),
                    ),
                  ),
                ],
              ),
            ],
            const SizedBox(height: 16),
            FilledButton.tonalIcon(
              onPressed: onAddZone,
              style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(44)),
              icon: const Icon(Icons.add_location_alt_outlined),
              label: Text(l10n.addZoneHere),
            ),
          ],
        ),
      ),
    );
  }
}

class _Metric extends StatelessWidget {
  const _Metric({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant)),
        const SizedBox(height: 2),
        Text(
          value,
          style: theme.textTheme.titleMedium?.copyWith(
            fontWeight: FontWeight.w600,
            fontFeatures: const [FontFeature.tabularFigures()],
          ),
        ),
      ],
    );
  }
}

class _ZonesCard extends StatelessWidget {
  const _ZonesCard({required this.zones, required this.state, required this.point, required this.onDelete});

  final List<Geofence> zones;
  final GeofenceEngineState? state;
  final GeoPoint? point;
  final ValueChanged<Geofence> onDelete;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context);
    if (zones.isEmpty) return _EmptyCard(icon: Icons.layers_outlined, message: l10n.noZones);

    final theme = Theme.of(context);
    final scheme = theme.colorScheme;
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        children: [
          for (var i = 0; i < zones.length; i++) ...[
            if (i > 0) Divider(height: 1, indent: 56, color: scheme.outlineVariant),
            Builder(
              builder: (context) {
                final zone = zones[i];
                final status = state?.regions[zone.id]?.status ?? GeofenceStatus.unknown;
                final (label, color, icon) = switch (status) {
                  GeofenceStatus.inside => (l10n.statusInside, _insideColor, Icons.where_to_vote),
                  GeofenceStatus.outside => (l10n.statusOutside, scheme.outline, Icons.location_on_outlined),
                  GeofenceStatus.unknown => (l10n.statusUnknown, scheme.tertiary, Icons.help_outline),
                };
                final radius = zone is CircleGeofence ? _meters(context, zone.radius) : '-';
                final distance = point == null ? null : zone.signedDistance(point!).abs();
                return ListTile(
                  leading: Icon(icon, color: color),
                  title: Text(
                    zone.name ?? zone.id,
                    style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600),
                  ),
                  subtitle: Text(
                    [
                      l10n.zoneDetails(radius),
                      if (distance != null) l10n.zoneDistance(_meters(context, distance)),
                    ].join('\n'),
                  ),
                  isThreeLine: distance != null,
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      _Badge(label: label, color: color),
                      IconButton(
                        tooltip: l10n.deleteZone,
                        onPressed: () => onDelete(zone),
                        icon: const Icon(Icons.delete_outline),
                      ),
                    ],
                  ),
                );
              },
            ),
          ],
        ],
      ),
    );
  }
}

class _EventsCard extends StatelessWidget {
  const _EventsCard({required this.events});

  static const int _maxVisible = 50;

  final List<GeofenceEvent> events;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context);
    if (events.isEmpty) return _EmptyCard(icon: Icons.receipt_long_outlined, message: l10n.noEvents);

    final theme = Theme.of(context);
    final scheme = theme.colorScheme;
    final visible = events.take(_maxVisible).toList();
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        children: [
          for (var i = 0; i < visible.length; i++) ...[
            if (i > 0) Divider(height: 1, indent: 56, color: scheme.outlineVariant),
            ListTile(
              dense: true,
              leading: Icon(
                switch (visible[i].type) {
                  GeofenceEventType.enter => Icons.login,
                  GeofenceEventType.exit => Icons.logout,
                  GeofenceEventType.dwell => Icons.hourglass_bottom,
                },
                color: switch (visible[i].type) {
                  GeofenceEventType.enter => _insideColor,
                  GeofenceEventType.exit => scheme.error,
                  GeofenceEventType.dwell => scheme.primary,
                },
              ),
              title: Text(
                _eventTitle(l10n, visible[i]),
                style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
              ),
              subtitle: Text(
                [
                  _formatTime(context, visible[i].time.toLocal()),
                  if (visible[i].initial) l10n.eventInitial,
                ].join('  ·  '),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class _AddZoneSheet extends StatefulWidget {
  const _AddZoneSheet({required this.defaultName});

  final String defaultName;

  @override
  State<_AddZoneSheet> createState() => _AddZoneSheetState();
}

class _AddZoneSheetState extends State<_AddZoneSheet> {
  static const List<double> _radii = [50, 100, 250, 500];

  late final TextEditingController _name = TextEditingController(text: widget.defaultName);
  double _radius = 100;

  @override
  void dispose() {
    _name.dispose();
    super.dispose();
  }

  void _submit() {
    final name = _name.text.trim();
    Navigator.of(context).pop((name.isEmpty ? widget.defaultName : name, _radius));
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context);
    final theme = Theme.of(context);
    return Padding(
      padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + MediaQuery.viewInsetsOf(context).bottom),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(l10n.addZoneTitle, style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700)),
          const SizedBox(height: 16),
          TextField(
            controller: _name,
            textCapitalization: TextCapitalization.sentences,
            decoration: InputDecoration(labelText: l10n.zoneName, border: const OutlineInputBorder()),
            onSubmitted: (_) => _submit(),
          ),
          const SizedBox(height: 16),
          Text(l10n.zoneRadius, style: theme.textTheme.labelLarge),
          const SizedBox(height: 8),
          SegmentedButton<double>(
            segments: [for (final r in _radii) ButtonSegment(value: r, label: Text(_meters(context, r)))],
            selected: {_radius},
            onSelectionChanged: (s) => setState(() => _radius = s.first),
          ),
          const SizedBox(height: 24),
          Row(
            children: [
              Expanded(
                child: OutlinedButton(
                  onPressed: () => Navigator.of(context).pop(),
                  style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(52)),
                  child: Text(l10n.cancel),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: FilledButton(onPressed: _submit, child: Text(l10n.add)),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _EmptyCard extends StatelessWidget {
  const _EmptyCard({required this.icon, required this.message});

  final IconData icon;
  final String message;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 16),
        child: Column(
          children: [
            Icon(icon, size: 32, color: scheme.outline),
            const SizedBox(height: 8),
            Text(
              message,
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
19
downloads

Documentation

API reference

Publisher

verified publisherabdurrahmankaraoglu.com

Weekly Downloads

Pure Dart geofencing engine: enter, exit and dwell events for circle and polygon zones from any location stream, with noise filters and saveable state.

Repository (GitHub)
View/report issues

Topics

#geofence #geofencing #location #maps #geolocation

License

MIT (license)

More

Packages that depend on advanced_geofence