Skip to main content

advanced_background_locator 0.1.4 copy "advanced_background_locator: ^0.1.4" to clipboard
advanced_background_locator: ^0.1.4 copied to clipboard

Flutter plugin for location tracking that keeps running in the background (Android foreground service, iOS background location updates).

example/lib/main.dart

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

import 'package:advanced_background_locator/advanced_background_locator.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 'file_manager.dart';
import 'l10n/app_localizations.dart';
import 'location_callback_handler.dart';
import 'location_service_repository.dart';

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

const Color _brandColor = Color(0xFF0B3D91);

class MyApp extends StatelessWidget {
  const MyApp({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 TrackerPage(),
    );
  }
}

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

  @override
  State<TrackerPage> createState() => _TrackerPageState();
}

class _TrackerPageState extends State<TrackerPage> {
  final ReceivePort port = ReceivePort();
  String logStr = '';
  bool isRunning = false;
  bool isBusy = false;
  LocationDto? lastLocation;

  @override
  void initState() {
    super.initState();

    // Arka planda çalışan izleyici için port kaydını kaldır
    if (IsolateNameServer.lookupPortByName(LocationServiceRepository.isolateName) != null) {
      IsolateNameServer.removePortNameMapping(LocationServiceRepository.isolateName);
    }

    // Yeni port kaydını oluştur
    IsolateNameServer.registerPortWithName(port.sendPort, LocationServiceRepository.isolateName);

    port.listen((dynamic data) async {
      debugPrint('Data: $data');
      await updateUI(data);
    }, onError: (error) {
      debugPrint('Error: $error');
    });

    initPlatformState();
  }

  Future<void> updateUI(dynamic data) async {
    final log = await FileManager.readLogFile();
    final LocationDto? locationDto = data is Map ? LocationDto.fromJson(data) : null;
    if (locationDto != null) {
      await _updateNotificationText(locationDto);
    }
    if (!mounted) return;

    setState(() {
      if (locationDto != null) {
        lastLocation = locationDto;
      }
      logStr = log;
    });
  }

  Future<void> _updateNotificationText(LocationDto data) async {
    if (!mounted) return;
    final l10n = AppLocalizations.of(context);
    await AdvancedBackgroundLocator.updateNotificationText(
      title: l10n.notificationTitle,
      msg: l10n.notificationLastUpdate(_formatTime(context, DateTime.now())),
      bigMsg: '${data.latitude.toStringAsFixed(5)}, ${data.longitude.toStringAsFixed(5)}',
    );
  }

  Future<void> initPlatformState() async {
    await AdvancedBackgroundLocator.initialize();
    final log = await FileManager.readLogFile();
    final isRunningResp = await AdvancedBackgroundLocator.isServiceRunning();
    if (!mounted) return;
    setState(() {
      logStr = log;
      isRunning = isRunningResp;
    });
  }

  @override
  void dispose() {
    IsolateNameServer.removePortNameMapping(LocationServiceRepository.isolateName);
    port.close();
    super.dispose();
  }

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

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

  Future<void> _clearLog() async {
    await FileManager.clearLogFile();
    if (!mounted) return;
    setState(() => logStr = '');
  }

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

  /// Requests foreground location first; background ("Allow all the time") must be
  /// requested separately on Android 11+ and iOS. Background access is optional:
  /// tracking still works while the foreground service notification is shown.
  Future<bool> _checkLocationPermission() async {
    final status = await Permission.locationWhenInUse.request();
    if (!status.isGranted) {
      return false;
    }

    // Android 13+: without this the foreground service notification is hidden.
    await Permission.notification.request();
    await Permission.locationAlways.request();
    return true;
  }

  Future<void> _startLocator() async {
    // Notification texts are sent to the native side, so they are resolved here in the UI locale.
    final l10n = AppLocalizations.of(context);
    final data = {'countInit': 1};
    await AdvancedBackgroundLocator.registerLocationUpdate(
      LocationCallbackHandler.callback,
      initCallback: LocationCallbackHandler.initCallback,
      initDataCallback: data,
      disposeCallback: LocationCallbackHandler.disposeCallback,
      iosSettings: const IOSSettings(
        accuracy: LocationAccuracy.NAVIGATION,
        distanceFilter: 0,
        stopWithTerminate: true,
      ),
      autoStop: false,
      androidSettings: AndroidSettings(
        accuracy: LocationAccuracy.NAVIGATION,
        interval: 3,
        distanceFilter: 0,
        client: LocationClient.android,
        androidNotificationSettings: AndroidNotificationSettings(
          notificationChannelName: l10n.notificationChannelName,
          notificationTitle: l10n.notificationTitle,
          notificationMsg: l10n.notificationMessage,
          notificationBigMsg: l10n.notificationBigMessage,
          notificationIconColor: _brandColor,
          notificationTapCallback: LocationCallbackHandler.notificationCallback,
        ),
      ),
    );
  }

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

    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.clearLog,
            onPressed: entries.isEmpty ? null : _clearLog,
            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.sectionLastLocation),
            const SizedBox(height: 8),
            _LocationCard(location: lastLocation, isRunning: isRunning),
            const SizedBox(height: 28),
            _SectionHeader(title: l10n.sectionActivityLog, trailing: entries.isEmpty ? null : l10n.logCount(entries.length)),
            const SizedBox(height: 8),
            _LogCard(entries: entries),
            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);

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 ? const Color(0xFF1B873F) : 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.my_location : 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),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
              decoration: BoxDecoration(
                color: accent.withValues(alpha: 0.12),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Container(width: 8, height: 8, decoration: BoxDecoration(color: accent, shape: BoxShape.circle)),
                  const SizedBox(width: 6),
                  Text(
                    isRunning ? l10n.badgeLive : l10n.badgeIdle,
                    style: theme.textTheme.labelMedium?.copyWith(color: accent, 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 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 ? AppLocalizations.of(context).stopTracking : AppLocalizations.of(context).startTracking),
    );
  }
}

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 _LocationCard extends StatelessWidget {
  const _LocationCard({required this.location, required this.isRunning});

  final LocationDto? location;
  final bool isRunning;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context);
    final loc = location;
    if (loc == null) {
      return _EmptyCard(
        icon: Icons.satellite_alt_outlined,
        message: isRunning ? l10n.waitingForLocation : l10n.noLocationYet,
      );
    }

    final time = DateTime.fromMillisecondsSinceEpoch(loc.time.toInt());
    // Coordinates keep the decimal point in every language so they can be copied as-is.
    final metrics = <(String, String)>[
      (l10n.metricLatitude, loc.latitude.toStringAsFixed(5)),
      (l10n.metricLongitude, loc.longitude.toStringAsFixed(5)),
      (l10n.metricAccuracy, '± ${l10n.unitMeters(_formatNumber(context, loc.accuracy, 0))}'),
      (l10n.metricSpeed, l10n.unitSpeed(_formatNumber(context, loc.speed * 3.6, 1))),
      (l10n.metricAltitude, l10n.unitMeters(_formatNumber(context, loc.altitude, 0))),
      (l10n.metricTime, _formatTime(context, time)),
    ];

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          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 (loc.isMocked || loc.provider.isNotEmpty) ...[
              const SizedBox(height: 16),
              const Divider(height: 1),
              const SizedBox(height: 12),
              Row(
                children: [
                  Icon(Icons.info_outline, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
                  const SizedBox(width: 6),
                  Text(
                    '${l10n.locationSource(loc.provider.isEmpty ? '-' : loc.provider)}${loc.isMocked ? '  ·  ${l10n.mockedLocation}' : ''}',
                    style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
                  ),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }
}

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 _LogEntry {
  const _LogEntry({required this.title, required this.subtitle, required this.kind});

  final String title;
  final String subtitle;
  final _LogKind kind;

  static final RegExp _positionLine = RegExp(r'^(-?\d+) : (\S+) --> (\S+) (\S+)');

  /// Parses the example's log file (newest first).
  static List<_LogEntry> parse(String log) {
    final entries = <_LogEntry>[];
    for (final raw in log.split('\n')) {
      final line = raw.trim();
      if (line.isEmpty || line.startsWith('---')) continue;
      if (line.startsWith('start:')) {
        entries.add(_LogEntry(title: '', subtitle: line.substring(6).trim(), kind: _LogKind.start));
      } else if (line.startsWith('end:')) {
        entries.add(_LogEntry(title: '', subtitle: line.substring(4).trim(), kind: _LogKind.end));
      } else {
        final match = _positionLine.firstMatch(line);
        if (match == null) continue;
        entries.add(_LogEntry(
          title: '${match.group(3)}, ${match.group(4)}',
          subtitle: '#${match.group(1)}  ·  ${match.group(2)}',
          kind: _LogKind.position,
        ));
      }
    }
    return entries.reversed.toList();
  }
}

enum _LogKind { start, end, position }

class _LogCard extends StatelessWidget {
  const _LogCard({required this.entries});

  static const int _maxVisible = 50;

  final List<_LogEntry> entries;

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

    final theme = Theme.of(context);
    final scheme = theme.colorScheme;
    final visible = entries.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].kind) {
                  _LogKind.start => Icons.play_circle_outline,
                  _LogKind.end => Icons.stop_circle_outlined,
                  _LogKind.position => Icons.place_outlined,
                },
                color: visible[i].kind == _LogKind.position ? scheme.primary : scheme.onSurfaceVariant,
              ),
              title: Text(
                switch (visible[i].kind) {
                  _LogKind.start => l10n.sessionStarted,
                  _LogKind.end => l10n.sessionEnded,
                  _LogKind.position => visible[i].title,
                },
                style: theme.textTheme.bodyMedium?.copyWith(
                  fontWeight: visible[i].kind == _LogKind.position ? FontWeight.w500 : FontWeight.w600,
                  fontFeatures: const [FontFeature.tabularFigures()],
                ),
              ),
              subtitle: Text(visible[i].subtitle),
            ),
          ],
          if (entries.length > _maxVisible)
            Padding(
              padding: const EdgeInsets.all(12),
              child: Text(
                l10n.olderEntries(entries.length - _maxVisible),
                style: theme.textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant),
              ),
            ),
        ],
      ),
    );
  }
}

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, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant)),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
267
downloads
screenshot

Documentation

API reference

Publisher

verified publisherabdurrahmankaraoglu.com

Weekly Downloads

Flutter plugin for location tracking that keeps running in the background (Android foreground service, iOS background location updates).

Repository (GitHub)
View/report issues

Topics

#location #background #geolocation

License

MIT (license)

Dependencies

flutter

More

Packages that depend on advanced_background_locator

Packages that implement advanced_background_locator