flutter_location_plus 1.0.0 copy "flutter_location_plus: ^1.0.0" to clipboard
flutter_location_plus: ^1.0.0 copied to clipboard

Native Android location plugin — GPS, Network & Passive providers. No Google Play Services. Replaces nativecode_location.

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_location_plus/flutter_location_plus.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_location_plus',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const _RootScreen(),
    );
  }
}

class _RootScreen extends StatelessWidget {
  const _RootScreen();

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
        appBar: AppBar(
          title: const Text('Location Tracker'),
          centerTitle: true,
          backgroundColor: Theme.of(context).colorScheme.primary,
          foregroundColor: Theme.of(context).colorScheme.onPrimary,
          bottom: TabBar(
            labelColor: Theme.of(context).colorScheme.onPrimary,
            unselectedLabelColor:
                Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.6),
            indicatorColor: Theme.of(context).colorScheme.onPrimary,
            tabs: const [
              Tab(icon: Icon(Icons.gps_fixed), text: 'One-Shot'),
              Tab(icon: Icon(Icons.radio_button_checked), text: 'Live'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [OneShotTab(), LiveTab()],
        ),
      ),
    );
  }
}

// ════════════════════════════════════════════════════════════════════════════
// ONE-SHOT TAB
// ════════════════════════════════════════════════════════════════════════════

enum _Status { idle, loading, success, error }

class _ProviderResult {
  final _Status status;
  final double? lat;
  final double? lng;
  final String? provider;
  final String? errorMsg;
  final DateTime? updatedAt;

  const _ProviderResult({
    this.status = _Status.idle,
    this.lat,
    this.lng,
    this.provider,
    this.errorMsg,
    this.updatedAt,
  });
}

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

  @override
  State<OneShotTab> createState() => _OneShotTabState();
}

class _OneShotTabState extends State<OneShotTab> {
  final _plugin = FlutterLocationPlus();

  late final Map<String, _ProviderResult> _results = {
    'gps': const _ProviderResult(),
    'network': const _ProviderResult(),
    'passive': const _ProviderResult(),
    'best': const _ProviderResult(),
  };

  Future<void> _fetch(String key) async {
    setState(() => _results[key] = const _ProviderResult(status: _Status.loading));
    try {
      final raw = await switch (key) {
        'gps' => _plugin.getGpsLocation(),
        'network' => _plugin.getNetworkLocation(),
        'passive' => _plugin.getPassiveLocation(),
        _ => _plugin.getBestLocation(),
      };
      final parts = raw?.split(',');
      if (parts != null && parts.length == 3 && parts[0] != 'null') {
        setState(() => _results[key] = _ProviderResult(
              status: _Status.success,
              lat: double.parse(parts[0]),
              lng: double.parse(parts[1]),
              provider: parts[2],
              updatedAt: DateTime.now(),
            ));
      } else {
        setState(() => _results[key] = _ProviderResult(
              status: _Status.error,
              errorMsg: _friendlyError(parts?.length == 3 ? parts![2] : 'unavailable'),
            ));
      }
    } catch (e) {
      setState(() => _results[key] =
          _ProviderResult(status: _Status.error, errorMsg: e.toString()));
    }
  }

  String _friendlyError(String code) => switch (code) {
        'disabled' => 'Provider is disabled on this device',
        'no_fix' => 'No fix yet — try again in a moment',
        'error' => 'Permission or system error',
        _ => 'Location unavailable',
      };

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _ProviderCard(
          title: 'GPS',
          subtitle: 'Satellite — most accurate outdoors',
          icon: Icons.satellite_alt,
          color: Colors.green,
          result: _results['gps']!,
          onFetch: () => _fetch('gps'),
        ),
        const SizedBox(height: 12),
        _ProviderCard(
          title: 'Network',
          subtitle: 'Cell towers & Wi-Fi — fast indoors',
          icon: Icons.cell_tower,
          color: Colors.blue,
          result: _results['network']!,
          onFetch: () => _fetch('network'),
        ),
        const SizedBox(height: 12),
        _ProviderCard(
          title: 'Passive',
          subtitle: 'Reuses fixes from other apps',
          icon: Icons.location_searching,
          color: Colors.orange,
          result: _results['passive']!,
          onFetch: () => _fetch('passive'),
        ),
        const SizedBox(height: 20),
        _BestCard(result: _results['best']!, onFetch: () => _fetch('best'), cs: cs),
      ],
    );
  }
}

// ════════════════════════════════════════════════════════════════════════════
// LIVE TAB
// ════════════════════════════════════════════════════════════════════════════

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

  @override
  State<LiveTab> createState() => _LiveTabState();
}

class _LiveTabState extends State<LiveTab> {
  final _plugin = FlutterLocationPlus();
  StreamSubscription<LocationData>? _sub;

  String _selectedProvider = 'best';
  int _intervalMs = 2000;
  double _distanceM = 0;

  bool get _isTracking => _sub != null;
  LocationData? _latest;
  final List<LocationData> _history = [];
  String? _error;

  void _start() {
    _error = null;
    _history.clear();
    _latest = null;
    _sub = _plugin
        .liveLocation(
          provider: _selectedProvider,
          intervalMs: _intervalMs,
          distanceMeters: _distanceM,
        )
        .listen(
          (data) => setState(() {
            _latest = data;
            _history.insert(0, data);
            if (_history.length > 50) _history.removeLast();
          }),
          onError: (e) => setState(() => _error = e.toString()),
        );
    setState(() {});
  }

  void _stop() {
    _sub?.cancel();
    _sub = null;
    _plugin.stopLiveLocation();
    setState(() {});
  }

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

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Column(
      children: [
        _LiveControlPanel(
          selectedProvider: _selectedProvider,
          intervalMs: _intervalMs,
          distanceM: _distanceM,
          isTracking: _isTracking,
          onProviderChanged: _isTracking ? null : (v) => setState(() => _selectedProvider = v!),
          onIntervalChanged: _isTracking ? null : (v) => setState(() => _intervalMs = v),
          onDistanceChanged: _isTracking ? null : (v) => setState(() => _distanceM = v),
          onStart: _start,
          onStop: _stop,
        ),
        if (_error != null)
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
            child: Row(children: [
              const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 16),
              const SizedBox(width: 6),
              Expanded(child: Text(_error!, style: const TextStyle(color: Colors.red, fontSize: 13))),
            ]),
          ),
        if (_latest != null) _LiveCurrentCard(data: _latest!, cs: cs),
        Expanded(child: _LiveHistoryList(history: _history)),
      ],
    );
  }
}

// ── Live Control Panel ────────────────────────────────────────────────────────

class _LiveControlPanel extends StatelessWidget {
  final String selectedProvider;
  final int intervalMs;
  final double distanceM;
  final bool isTracking;
  final ValueChanged<String?>? onProviderChanged;
  final ValueChanged<int>? onIntervalChanged;
  final ValueChanged<double>? onDistanceChanged;
  final VoidCallback onStart;
  final VoidCallback onStop;

  const _LiveControlPanel({
    required this.selectedProvider,
    required this.intervalMs,
    required this.distanceM,
    required this.isTracking,
    required this.onProviderChanged,
    required this.onIntervalChanged,
    required this.onDistanceChanged,
    required this.onStart,
    required this.onStop,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Card(
      margin: const EdgeInsets.all(16),
      elevation: 0,
      color: cs.surfaceContainer,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Provider selector
            Row(
              children: [
                const Text('Provider', style: TextStyle(fontWeight: FontWeight.w600)),
                const SizedBox(width: 12),
                Expanded(
                  child: DropdownButtonFormField<String>(
                    value: selectedProvider,
                    isDense: true,
                    decoration: InputDecoration(
                      contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                      border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
                    ),
                    items: const [
                      DropdownMenuItem(value: 'best', child: Text('Best (auto)')),
                      DropdownMenuItem(value: 'gps', child: Text('GPS only')),
                      DropdownMenuItem(value: 'network', child: Text('Network only')),
                      DropdownMenuItem(value: 'passive', child: Text('Passive only')),
                    ],
                    onChanged: onProviderChanged,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            // Interval slider
            Row(
              children: [
                const SizedBox(width: 4),
                const Icon(Icons.timer_outlined, size: 18, color: Colors.grey),
                const SizedBox(width: 6),
                Text('Interval: ${intervalMs}ms',
                    style: const TextStyle(fontSize: 13, color: Colors.grey)),
                Expanded(
                  child: Slider(
                    value: intervalMs.toDouble(),
                    min: 500,
                    max: 10000,
                    divisions: 19,
                    onChanged: onIntervalChanged != null
                        ? (v) => onIntervalChanged!(v.round())
                        : null,
                  ),
                ),
              ],
            ),
            // Distance slider
            Row(
              children: [
                const SizedBox(width: 4),
                const Icon(Icons.straighten, size: 18, color: Colors.grey),
                const SizedBox(width: 6),
                Text('Min dist: ${distanceM.round()}m',
                    style: const TextStyle(fontSize: 13, color: Colors.grey)),
                Expanded(
                  child: Slider(
                    value: distanceM,
                    min: 0,
                    max: 100,
                    divisions: 20,
                    onChanged: onDistanceChanged != null
                        ? (v) => onDistanceChanged!(v)
                        : null,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 4),
            SizedBox(
              width: double.infinity,
              child: isTracking
                  ? FilledButton.icon(
                      style: FilledButton.styleFrom(backgroundColor: Colors.red),
                      onPressed: onStop,
                      icon: const Icon(Icons.stop_circle_outlined),
                      label: const Text('Stop Tracking'),
                    )
                  : FilledButton.icon(
                      onPressed: onStart,
                      icon: const Icon(Icons.radio_button_checked),
                      label: const Text('Start Live Tracking'),
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

// ── Live Current Card ─────────────────────────────────────────────────────────

class _LiveCurrentCard extends StatelessWidget {
  final LocationData data;
  final ColorScheme cs;

  const _LiveCurrentCard({required this.data, required this.cs});

  static const _providerColor = {
    'gps': Colors.green,
    'network': Colors.blue,
    'passive': Colors.orange,
  };

  @override
  Widget build(BuildContext context) {
    final color = _providerColor[data.provider] ?? Colors.indigo;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Card(
        elevation: 0,
        color: cs.primaryContainer,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Row(children: [
                    Icon(Icons.my_location, color: cs.primary, size: 18),
                    const SizedBox(width: 6),
                    Text('Live Location',
                        style: TextStyle(
                            fontWeight: FontWeight.bold, color: cs.primary)),
                  ]),
                  Chip(
                    label: Text(data.provider.toUpperCase(),
                        style: TextStyle(
                            color: color,
                            fontWeight: FontWeight.bold,
                            fontSize: 11)),
                    backgroundColor: color.withValues(alpha: 0.1),
                    side: BorderSide(color: color.withValues(alpha: 0.3)),
                    padding: EdgeInsets.zero,
                    visualDensity: VisualDensity.compact,
                  ),
                ],
              ),
              const SizedBox(height: 12),
              Row(
                children: [
                  Expanded(child: _StatTile(label: 'Latitude', value: data.lat.toStringAsFixed(6))),
                  Expanded(child: _StatTile(label: 'Longitude', value: data.lng.toStringAsFixed(6))),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                      child: _StatTile(
                          label: 'Accuracy',
                          value: '±${data.accuracy.toStringAsFixed(1)}m')),
                  Expanded(
                      child: _StatTile(
                          label: 'Speed',
                          value: '${(data.speed * 3.6).toStringAsFixed(1)} km/h')),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _StatTile extends StatelessWidget {
  final String label;
  final String value;
  const _StatTile({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label,
            style: const TextStyle(fontSize: 11, color: Colors.grey)),
        Text(value,
            style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
      ],
    );
  }
}

// ── Live History List ─────────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    if (history.isEmpty) {
      return Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.history, size: 48, color: Colors.grey.shade300),
            const SizedBox(height: 8),
            Text('No updates yet',
                style: TextStyle(color: Colors.grey.shade400)),
          ],
        ),
      );
    }
    return ListView.builder(
      padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
      itemCount: history.length,
      itemBuilder: (context, i) {
        final d = history[i];
        final isLatest = i == 0;
        return Padding(
          padding: const EdgeInsets.only(bottom: 6),
          child: ListTile(
            dense: true,
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
            tileColor: isLatest
                ? Theme.of(context).colorScheme.surfaceContainerHigh
                : Theme.of(context).colorScheme.surfaceContainer,
            leading: CircleAvatar(
              radius: 14,
              backgroundColor: isLatest
                  ? Theme.of(context).colorScheme.primary
                  : Colors.grey.shade300,
              child: Text(
                '${history.length - i}',
                style: TextStyle(
                    fontSize: 10,
                    color: isLatest ? Colors.white : Colors.grey.shade700),
              ),
            ),
            title: Text(
              '${d.lat.toStringAsFixed(5)}, ${d.lng.toStringAsFixed(5)}',
              style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
            ),
            subtitle: Text(
              '${d.provider} · ±${d.accuracy.toStringAsFixed(1)}m · ${(d.speed * 3.6).toStringAsFixed(1)} km/h',
              style: const TextStyle(fontSize: 11),
            ),
            trailing: Text(
              _formatTime(d.time),
              style: const TextStyle(fontSize: 11, color: Colors.grey),
            ),
          ),
        );
      },
    );
  }

  String _formatTime(DateTime t) {
    final now = DateTime.now();
    final diff = now.difference(t).inSeconds;
    if (diff < 5) return 'now';
    if (diff < 60) return '${diff}s ago';
    return '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}';
  }
}

// ════════════════════════════════════════════════════════════════════════════
// ONE-SHOT SHARED WIDGETS
// ════════════════════════════════════════════════════════════════════════════

class _ProviderCard extends StatelessWidget {
  final String title;
  final String subtitle;
  final IconData icon;
  final Color color;
  final _ProviderResult result;
  final VoidCallback onFetch;

  const _ProviderCard({
    required this.title,
    required this.subtitle,
    required this.icon,
    required this.color,
    required this.result,
    required this.onFetch,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Card(
      elevation: 0,
      color: cs.surfaceContainer,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                CircleAvatar(
                  backgroundColor: color.withValues(alpha: 0.15),
                  child: Icon(icon, color: color, size: 20),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(title,
                          style: Theme.of(context)
                              .textTheme
                              .titleMedium
                              ?.copyWith(fontWeight: FontWeight.bold)),
                      Text(subtitle,
                          style: Theme.of(context)
                              .textTheme
                              .bodySmall
                              ?.copyWith(color: Colors.grey)),
                    ],
                  ),
                ),
                _FetchIconButton(
                    loading: result.status == _Status.loading,
                    color: color,
                    onTap: onFetch),
              ],
            ),
            const SizedBox(height: 12),
            _ResultBody(result: result, color: color),
          ],
        ),
      ),
    );
  }
}

class _BestCard extends StatelessWidget {
  final _ProviderResult result;
  final VoidCallback onFetch;
  final ColorScheme cs;

  const _BestCard(
      {required this.result, required this.onFetch, required this.cs});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 0,
      color: cs.primaryContainer,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                CircleAvatar(
                  backgroundColor: cs.primary.withValues(alpha: 0.15),
                  child: Icon(Icons.auto_awesome, color: cs.primary, size: 20),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Best Available',
                          style: Theme.of(context)
                              .textTheme
                              .titleMedium
                              ?.copyWith(fontWeight: FontWeight.bold)),
                      Text('GPS → Network → Passive fallback',
                          style: Theme.of(context)
                              .textTheme
                              .bodySmall
                              ?.copyWith(color: Colors.grey.shade600)),
                    ],
                  ),
                ),
                _FetchIconButton(
                    loading: result.status == _Status.loading,
                    color: cs.primary,
                    onTap: onFetch),
              ],
            ),
            const SizedBox(height: 12),
            _ResultBody(result: result, color: cs.primary),
          ],
        ),
      ),
    );
  }
}

class _ResultBody extends StatelessWidget {
  final _ProviderResult result;
  final Color color;
  const _ResultBody({required this.result, required this.color});

  @override
  Widget build(BuildContext context) {
    return switch (result.status) {
      _Status.idle => const Text('Tap the button to fetch',
          style: TextStyle(color: Colors.grey, fontSize: 13)),
      _Status.loading => const Center(
          child: Padding(
            padding: EdgeInsets.symmetric(vertical: 8),
            child: CircularProgressIndicator(strokeWidth: 2),
          )),
      _Status.error => Row(children: [
          const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 16),
          const SizedBox(width: 6),
          Expanded(
              child: Text(result.errorMsg ?? 'Unknown error',
                  style: const TextStyle(color: Colors.red, fontSize: 13))),
        ]),
      _Status.success => Column(children: [
          _KV(label: 'Latitude', value: result.lat!.toStringAsFixed(6)),
          const SizedBox(height: 4),
          _KV(label: 'Longitude', value: result.lng!.toStringAsFixed(6)),
          const SizedBox(height: 4),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text('Provider',
                  style: Theme.of(context)
                      .textTheme
                      .bodySmall
                      ?.copyWith(color: Colors.grey)),
              Chip(
                label: Text(result.provider!.toUpperCase(),
                    style: TextStyle(
                        color: color,
                        fontWeight: FontWeight.bold,
                        fontSize: 11)),
                backgroundColor: color.withValues(alpha: 0.1),
                side: BorderSide(color: color.withValues(alpha: 0.3)),
                padding: EdgeInsets.zero,
                visualDensity: VisualDensity.compact,
              ),
            ],
          ),
          if (result.updatedAt != null)
            Align(
              alignment: Alignment.centerRight,
              child: Text(
                'Updated ${_ago(result.updatedAt!)}',
                style: const TextStyle(fontSize: 11, color: Colors.grey),
              ),
            ),
        ]),
    };
  }

  String _ago(DateTime t) {
    final s = DateTime.now().difference(t).inSeconds;
    if (s < 5) return 'just now';
    if (s < 60) return '${s}s ago';
    return '${s ~/ 60}m ago';
  }
}

class _KV extends StatelessWidget {
  final String label;
  final String value;
  const _KV({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(label,
            style: Theme.of(context)
                .textTheme
                .bodySmall
                ?.copyWith(color: Colors.grey)),
        Text(value,
            style: Theme.of(context)
                .textTheme
                .bodyMedium
                ?.copyWith(fontWeight: FontWeight.w600)),
      ],
    );
  }
}

class _FetchIconButton extends StatelessWidget {
  final bool loading;
  final Color color;
  final VoidCallback onTap;
  const _FetchIconButton(
      {required this.loading, required this.color, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return IconButton.filled(
      style: IconButton.styleFrom(backgroundColor: color.withValues(alpha: 0.15)),
      onPressed: loading ? null : onTap,
      icon: loading
          ? SizedBox(
              width: 18,
              height: 18,
              child: CircularProgressIndicator(strokeWidth: 2, color: color))
          : Icon(Icons.refresh, color: color),
    );
  }
}
1
likes
0
points
114
downloads

Publisher

unverified uploader

Weekly Downloads

Native Android location plugin — GPS, Network & Passive providers. No Google Play Services. Replaces nativecode_location.

Repository (GitHub)
View/report issues

Topics

#location #gps #android #geolocation #native

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_location_plus

Packages that implement flutter_location_plus