flutter_native_location 0.2.0 copy "flutter_native_location: ^0.2.0" to clipboard
flutter_native_location: ^0.2.0 copied to clipboard

Native location tracking for Flutter with configurable interval and accuracy filter.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_native_location/flutter_native_location.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Flutter Native Location',
    theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
    home: const TrackerPage(),
  );
}

class TrackerPage extends StatefulWidget {
  const TrackerPage({super.key});
  @override
  State<TrackerPage> createState() => _TrackerPageState();
}

class _TrackerPageState extends State<TrackerPage> {
  static const _config = LocationConfig(
    accuracy: LocationAccuracy.high, // filter fixes worse than ~25 m
  );

  StreamSubscription<Position>? _sub;
  bool _isTracking = false;
  Position? _latest;
  final List<Position> _history = [];
  String? _errorMessage;

  // ── Actions ────────────────────────────────────────────────────────────────

  void _startTracking() {
    if (_isTracking) return;
    _sub = FlutterNativeLocation.getLocationStream(_config).listen(
      (point) {
        setState(() {
          _latest = point;
          _history.insert(0, point);
          _errorMessage = null;
        });
      },
      onError: (Object error) {
        final msg = error is PlatformException
            ? '[${error.code}] ${error.message}'
            : error.toString();
        setState(() => _errorMessage = msg);
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Location error: $msg'),
              backgroundColor: Colors.red,
            ),
          );
        }
      },
    );
    setState(() => _isTracking = true);
  }

  Future<void> _stopTracking() async {
    await _sub?.cancel();
    _sub = null;
    setState(() => _isTracking = false);
  }

  Future<void> _getLastLocation() async {
    try {
      final loc = await FlutterNativeLocation.getLastLocation();
      if (loc != null) {
        setState(() {
          _latest = loc;
          _history.insert(0, loc);
          _errorMessage = null;
        });
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Got last known location')),
          );
        }
      } else {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('No cached location available')),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
        );
      }
    }
  }

  Future<void> _getCurrentLocation() async {
    try {
      final loc = await FlutterNativeLocation.getCurrentLocation();
      if (loc != null) {
        setState(() {
          _latest = loc;
          _history.insert(0, loc);
          _errorMessage = null;
        });
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Got current location')),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
        );
      }
    }
  }

  @override
  void dispose() {
    _sub?.cancel();
    super.dispose();
  }

  // ── UI ─────────────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Native Location'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.location_on), text: 'Live'),
              Tab(icon: Icon(Icons.list), text: 'History'),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            _LiveTab(
              isTracking: _isTracking,
              config: _config,
              latest: _latest,
              errorMessage: _errorMessage,
              onStart: _startTracking,
              onStop: _stopTracking,
              onGetLastLocation: _getLastLocation,
              onGetCurrentLocation: _getCurrentLocation,
            ),
            _HistoryTab(history: _history),
          ],
        ),
      ),
    );
  }
}

// ── Live Tab ──────────────────────────────────────────────────────────────────

class _LiveTab extends StatelessWidget {
  final bool isTracking;
  final LocationConfig config;
  final Position? latest;
  final String? errorMessage;
  final VoidCallback onStart;
  final Future<void> Function() onStop;
  final VoidCallback onGetLastLocation;
  final VoidCallback onGetCurrentLocation;

  const _LiveTab({
    required this.isTracking,
    required this.config,
    required this.latest,
    required this.errorMessage,
    required this.onStart,
    required this.onStop,
    required this.onGetLastLocation,
    required this.onGetCurrentLocation,
  });

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _StatusCard(isTracking: isTracking, config: config),
          if (errorMessage != null) ...[
            const SizedBox(height: 8),
            _ErrorBanner(message: errorMessage!),
          ],
          const SizedBox(height: 12),
          if (latest != null) _LocationCard(point: latest!),
          const SizedBox(height: 12),
          _TrackingButton(isTracking: isTracking, onStart: onStart, onStop: onStop),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: onGetLastLocation,
                  icon: const Icon(Icons.history, size: 18),
                  label: const Text('Last Known'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: onGetCurrentLocation,
                  icon: const Icon(Icons.gps_fixed, size: 18),
                  label: const Text('Current Fix'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  final bool isTracking;
  final LocationConfig config;
  const _StatusCard({required this.isTracking, required this.config});

  @override
  Widget build(BuildContext context) {
    final color = isTracking ? Colors.green : Colors.grey;
    return Card(
      child: ListTile(
        leading: AnimatedContainer(
          duration: const Duration(milliseconds: 400),
          width: 12,
          height: 12,
          decoration: BoxDecoration(
            color: color,
            shape: BoxShape.circle,
            boxShadow: isTracking
                ? [BoxShadow(color: Colors.green.withAlpha(120), blurRadius: 8, spreadRadius: 2)]
                : null,
          ),
        ),
        title: Text(
          isTracking ? 'Tracking' : 'Idle',
          style: TextStyle(color: color, fontWeight: FontWeight.bold),
        ),
        subtitle: Text(
          '${config.accuracy.name} accuracy  •  ≤${config.resolvedAccuracyFilter.toStringAsFixed(0)} m filter',
          style: const TextStyle(fontSize: 12),
        ),
      ),
    );
  }
}

class _ErrorBanner extends StatelessWidget {
  final String message;
  const _ErrorBanner({required this.message});
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        color: Colors.red.shade50,
        border: Border.all(color: Colors.red.shade200),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        children: [
          const Icon(Icons.error_outline, color: Colors.red, size: 18),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              message,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ),
        ],
      ),
    );
  }
}

class _TrackingButton extends StatelessWidget {
  final bool isTracking;
  final VoidCallback onStart;
  final Future<void> Function() onStop;
  const _TrackingButton({
    required this.isTracking,
    required this.onStart,
    required this.onStop,
  });
  @override
  Widget build(BuildContext context) {
    if (isTracking) {
      return FilledButton.icon(
        onPressed: onStop,
        icon: const Icon(Icons.stop_circle_outlined),
        label: const Text('Stop Tracking'),
        style: FilledButton.styleFrom(backgroundColor: Colors.red),
      );
    }
    return FilledButton.icon(
      onPressed: onStart,
      icon: const Icon(Icons.location_on),
      label: const Text('Start Tracking'),
    );
  }
}

class _LocationCard extends StatelessWidget {
  final Position point;
  const _LocationCard({required this.point});

  @override
  Widget build(BuildContext context) {
    final tt = Theme.of(context).textTheme;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Latest Position', style: tt.titleMedium),
            const Divider(),
            _Row('Latitude', '${(point.latitude ?? 0).toStringAsFixed(6)}°'),
            _Row('Longitude', '${(point.longitude ?? 0).toStringAsFixed(6)}°'),
            _Row('Accuracy', '±${(point.accuracy ?? 0).toStringAsFixed(1)} m'),
            _Row('Altitude', '${(point.altitude ?? 0).toStringAsFixed(1)} m'),
            _Row(
              'Alt. Accuracy',
              '±${(point.altitudeAccuracy ?? 0).toStringAsFixed(1)} m',
            ),
            _Row(
              'Heading',
              point.heading != null && point.heading! >= 0
                  ? '${point.heading!.toStringAsFixed(1)}°'
                  : 'N/A',
            ),
            _Row(
              'Heading Acc.',
              point.headingAccuracy != null && point.headingAccuracy! >= 0
                  ? '±${point.headingAccuracy!.toStringAsFixed(1)}°'
                  : 'N/A',
            ),
            _Row('Speed', '${(point.speedKmh ?? 0).toStringAsFixed(1)} km/h'),
            _Row(
              'Speed Acc.',
              point.speedAccuracy != null && point.speedAccuracy! >= 0
                  ? '±${point.speedAccuracy!.toStringAsFixed(1)} m/s'
                  : 'N/A',
            ),
            _Row(
              'Timestamp',
              point.timestamp != null
                  ? point.timestamp!.toLocal().toString().substring(0, 19)
                  : 'N/A',
            ),
          ],
        ),
      ),
    );
  }
}

class _Row extends StatelessWidget {
  final String label, value;
  const _Row(this.label, this.value);
  @override
  Widget build(BuildContext context) => Padding(
    padding: const EdgeInsets.symmetric(vertical: 3),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(label, style: const TextStyle(color: Colors.grey)),
        Text(
          value,
          style: const TextStyle(
            fontWeight: FontWeight.w500,
            fontFamily: 'monospace',
          ),
        ),
      ],
    ),
  );
}

// ── History Tab ───────────────────────────────────────────────────────────────

class _HistoryTab extends StatelessWidget {
  final List<Position> history;
  const _HistoryTab({required this.history});

  @override
  Widget build(BuildContext context) {
    if (history.isEmpty) {
      return const Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.map_outlined, size: 64, color: Colors.grey),
            SizedBox(height: 12),
            Text('No location history yet', style: TextStyle(color: Colors.grey)),
            SizedBox(height: 4),
            Text(
              'Start tracking to collect points',
              style: TextStyle(color: Colors.grey, fontSize: 12),
            ),
          ],
        ),
      );
    }
    return ListView.builder(
      itemCount: history.length,
      itemBuilder: (_, i) {
        final p = history[i];
        return ListTile(
          leading: const Icon(Icons.location_pin, color: Colors.indigo),
          title: Text(
            '${(p.latitude ?? 0).toStringAsFixed(5)},  ${(p.longitude ?? 0).toStringAsFixed(5)}',
            style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
          ),
          subtitle: Text(
            '±${(p.accuracy ?? 0).toStringAsFixed(0)} m  •  '
            '${(p.speedKmh ?? 0).toStringAsFixed(1)} km/h  •  '
            '${p.timestamp?.toLocal().toIso8601String().substring(11, 19) ?? '--'}',
          ),
        );
      },
    );
  }
}
0
likes
0
points
56
downloads

Publisher

verified publisheryanuar.id

Weekly Downloads

Native location tracking for Flutter with configurable interval and accuracy filter.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, json_annotation, plugin_platform_interface

More

Packages that depend on flutter_native_location

Packages that implement flutter_native_location