flutter_native_location 0.1.0 copy "flutter_native_location: ^0.1.0" to clipboard
flutter_native_location: ^0.1.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() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialise the singleton once — permission requested + autoStart begins
  // tracking immediately.
  await FlutterNativeLocation.init(
    LocationConfig(
      intervalSeconds: 5,
      accuracy: LocationAccuracy.high, // ≤ 25 m
      autoStart: true,
    ),
  );

  runApp(const MyApp());
}

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

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

class _TrackerPageState extends State<TrackerPage> {
  // Access the singleton — already initialised in main()
  FlutterNativeLocation get _plugin => FlutterNativeLocation.instance;

  StreamSubscription<Position>? _sub;
  late TrackingState _state;
  Position? _latest;
  final List<Position> _history = [];
  String? _errorMessage;

  // ── Lifecycle ──────────────────────────────────────────────────────────────

  @override
  void initState() {
    super.initState();
    // Sync local state from singleton (may already be tracking via autoStart)
    _state = _plugin.state;
    // Subscribe to the stream — single broadcast stream shared across the app
    _sub = _plugin.locationStream.listen(
      (point) {
        setState(() {
          _latest = point;
          _history.insert(0, point);
          _errorMessage = null;
        });
      },
      onError: (Object error) {
        // Native layer sends PlatformException on CLLocationManager errors
        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,
            ),
          );
        }
      },
    );
  }

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

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

  Future<void> _start() async {
    // Uses config from init() by default — no need to repeat intervalSeconds
    await _plugin.startTracking();
    setState(() => _state = TrackingState.tracking);
  }

  Future<void> _pause() async {
    await _plugin.pauseTracking();
    setState(() => _state = TrackingState.paused);
  }

  Future<void> _resume() async {
    await _plugin.resumeTracking();
    setState(() => _state = TrackingState.tracking);
  }

  Future<void> _stop() async {
    await _plugin.stopTracking();
    setState(() => _state = TrackingState.idle);
  }

  // ── 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(
              state: _state,
              latest: _latest,
              config: _plugin.config,
              errorMessage: _errorMessage,
              onStart: _start,
              onPause: _pause,
              onResume: _resume,
              onStop: _stop,
            ),
            _HistoryTab(history: _history),
          ],
        ),
      ),
    );
  }
}

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

class _LiveTab extends StatelessWidget {
  final TrackingState state;
  final Position? latest;
  final LocationConfig config;
  final String? errorMessage;
  final VoidCallback onStart, onPause, onResume, onStop;

  const _LiveTab({
    required this.state,
    required this.latest,
    required this.config,
    required this.errorMessage,
    required this.onStart,
    required this.onPause,
    required this.onResume,
    required this.onStop,
  });

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _StatusBadge(state: state, config: config),
          if (errorMessage != null) ...[
            const SizedBox(height: 8),
            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(
                      errorMessage!,
                      style: const TextStyle(color: Colors.red, fontSize: 12),
                    ),
                  ),
                ],
              ),
            ),
          ],
          const SizedBox(height: 16),
          if (latest != null) _LocationCard(point: latest!),
          const SizedBox(height: 16),
          _ControlButtons(
            state: state,
            onStart: onStart,
            onPause: onPause,
            onResume: onResume,
            onStop: onStop,
          ),
        ],
      ),
    );
  }
}

class _StatusBadge extends StatelessWidget {
  final TrackingState state;
  final LocationConfig config;
  const _StatusBadge({required this.state, required this.config});

  Color get _color => switch (state) {
    TrackingState.tracking => Colors.green,
    TrackingState.paused => Colors.orange,
    TrackingState.error => Colors.red,
    _ => Colors.grey,
  };

  String get _label => switch (state) {
    TrackingState.tracking => 'Tracking',
    TrackingState.paused => 'Paused',
    TrackingState.error => 'Error',
    _ => 'Idle',
  };

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: Icon(Icons.circle, color: _color, size: 14),
        title: Text(
          _label,
          style: TextStyle(color: _color, fontWeight: FontWeight.bold),
        ),
        trailing: Text(
          'every ${config.intervalSeconds}s  •  ${config.accuracy.name} (≤${config.resolvedAccuracyFilter.toStringAsFixed(0)}m)',
          style: const TextStyle(color: Colors.grey, fontSize: 12),
        ),
      ),
    );
  }
}

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('Current Location', 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(
              'Altitude Acc.',
              '±${(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 ?? DateTime.now()).toLocal().toString(),
            ),
          ],
        ),
      ),
    );
  }
}

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',
          ),
        ),
      ],
    ),
  );
}

class _ControlButtons extends StatelessWidget {
  final TrackingState state;
  final VoidCallback onStart, onPause, onResume, onStop;
  const _ControlButtons({
    required this.state,
    required this.onStart,
    required this.onPause,
    required this.onResume,
    required this.onStop,
  });

  @override
  Widget build(BuildContext context) {
    return switch (state) {
      TrackingState.tracking => Row(
        children: [
          Expanded(
            child: OutlinedButton.icon(
              onPressed: onPause,
              icon: const Icon(Icons.pause),
              label: const Text('Pause'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: FilledButton.icon(
              onPressed: onStop,
              icon: const Icon(Icons.stop),
              label: const Text('Stop'),
              style: FilledButton.styleFrom(backgroundColor: Colors.red),
            ),
          ),
        ],
      ),
      TrackingState.paused => Row(
        children: [
          Expanded(
            child: FilledButton.icon(
              onPressed: onResume,
              icon: const Icon(Icons.play_arrow),
              label: const Text('Resume'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: FilledButton.icon(
              onPressed: onStop,
              icon: const Icon(Icons.stop),
              label: const Text('Stop'),
              style: FilledButton.styleFrom(backgroundColor: Colors.red),
            ),
          ),
        ],
      ),
      _ => FilledButton.icon(
        onPressed: onStart,
        icon: const Icon(Icons.location_on),
        label: const Text('Start Tracking'),
      ),
    };
  }
}

// ── 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 history yet', style: TextStyle(color: Colors.grey)),
          ],
        ),
      );
    }
    return ListView.builder(
      itemCount: history.length,
      itemBuilder: (_, i) {
        final p = history[i];
        return ListTile(
          leading: const Icon(Icons.location_pin, color: Colors.blue),
          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 ?? DateTime.now()).toLocal().toIso8601String().substring(11, 19)}',
          ),
        );
      },
    );
  }
}
0
likes
150
points
86
downloads

Documentation

API reference

Publisher

verified publisheryanuar.id

Weekly Downloads

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

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, json_annotation, plugin_platform_interface

More

Packages that depend on flutter_native_location

Packages that implement flutter_native_location