adaptive_location_tracker 0.1.3 copy "adaptive_location_tracker: ^0.1.3" to clipboard
adaptive_location_tracker: ^0.1.3 copied to clipboard

A battery-efficient adaptive location tracker with offline sync, Kalman filtering, and foreground service support.

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:adaptive_location_tracker/adaptive_location_tracker.dart';
import 'package:permission_handler/permission_handler.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.deepPurple, useMaterial3: true),
      home: const TrackerHome(),
    );
  }
}

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

  @override
  State<TrackerHome> createState() => _TrackerHomeState();
}

class _TrackerHomeState extends State<TrackerHome> {
  final _urlController = TextEditingController(
    text: 'https://your-api.com/v1/trace',
  );
  final _userIdController = TextEditingController(text: '101');
  bool _isTracking = false;
  bool _isWebSocket = false;
  String _statusLog = "Ready to track.";
  StreamSubscription<Map<String, dynamic>>? _eventsSubscription;

  @override
  void initState() {
    super.initState();
    _eventsSubscription = AdaptiveLocationTracker.eventsStream.listen(
      (event) {
        _handleTrackerEvent(event);
      },
      onError: (err) {
        _log("❌ Stream Error: $err");
      },
    );
  }

  @override
  void dispose() {
    _eventsSubscription?.cancel();
    _urlController.dispose();
    _userIdController.dispose();
    super.dispose();
  }

  void _log(String message) {
    setState(() {
      _statusLog = "$message\n$_statusLog";
    });
  }

  void _handleTrackerEvent(Map<String, dynamic> event) {
    final type = event['type'] as String?;

    switch (type) {
      case 'location':
        _log(
          "📍 ${event['latitude']}, ${event['longitude']} | acc=${event['accuracy']} | state=${event['state']}",
        );
        break;
      case 'trip_state':
        _log("🚗 Trip state: ${event['state']}");
        break;
      case 'motion_state':
        _log(
          "🏃 Motion: ${event['isMoving'] == true ? 'MOVING' : 'STATIONARY'}",
        );
        break;
      case 'sync_success':
        _log("✅ Synced ${event['count']} records");
        break;
      case 'checkout_success':
        setState(() {
          _isTracking = false;
        });
        _log("✅ ${event['message']}");
        break;
      case 'error':
        final code = event['code'];
        final message = event['message'];
        if (code == 'CHECKOUT_FLUSH_FAILED') {
          setState(() {
            _isTracking = true;
          });
        }
        _log("❌ [$code] $message");
        break;
      default:
        _log("📱 Event: $event");
    }
  }

  Future<bool> _checkPermissions() async {
    _log("🔒 Checking permissions...");

    Map<Permission, PermissionStatus> statuses = await [
      Permission.location,
      Permission.activityRecognition,
      Permission.notification,
    ].request();

    if (!statuses[Permission.location]!.isGranted) {
      _log("❌ Basic Location permission is required.");
      return false;
    }

    // MANDATORY: Location Always (Background Location)
    _log("🔒 Requesting 'Always' Location permission...");
    final alwaysStatus = await Permission.locationAlways.request();
    if (!alwaysStatus.isGranted) {
      _log(
        "❌ 'Always' Location permission is mandatory for background tracking.",
      );
      return false;
    }

    // OPTIONAL: Ignore Battery Optimizations
    if (!await Permission.ignoreBatteryOptimizations.isGranted) {
      _log(
        "🔋 Requesting to ignore battery optimizations (Optional but recommended)...",
      );
      await Permission.ignoreBatteryOptimizations.request();
    }

    return true;
  }

  Future<void> _toggleTracking() async {
    if (_isTracking) {
      await AdaptiveLocationTracker.stop();
      setState(() {
        _isTracking = false;
      });
      _log("🛑 Stopped Tracking");
    } else {
      _log("⏳ Starting...");

      // Check permissions first
      final hasPermission = await _checkPermissions();
      if (!hasPermission) {
        _log("❌ Cannot start without permissions");
        return;
      }

      try {
        final success = await AdaptiveLocationTracker.start(
          url: _urlController.text,
          headers: {
            'Authorization': 'Bearer SAMPLE_TOKEN_123',
            'Content-Type': 'application/json',
          },
          extraData: {
            'user_id': _userIdController.text,
            'app_version': '1.0.0',
            'platform': 'android',
          },
          notificationTitle: _isWebSocket
              ? "Live Socket Tracking"
              : "Field Visit Active",
          notificationText: "Tap to return to app",
          reportLocationUrl: 'https://your-api.com/v1/report-location',
          isWebSocket: _isWebSocket,
          intervalSeconds: 5, // Fast updates for testing
        );

        if (success) {
          setState(() {
            _isTracking = true;
          });
          _log(
            "✅ Started Successfully (${_isWebSocket ? 'WebSocket' : 'HTTP'})",
          );
        }
      } catch (e) {
        _log("❌ Error starting: $e");
      }
    }
  }

  Future<void> _checkout() async {
    _log("🏁 Initiating Checkout...");
    final success = await AdaptiveLocationTracker.checkout();
    if (success) {
      _log("⏳ Checkout started. Waiting for final event...");
    } else {
      _log("❌ Checkout Failed");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Adaptive Tracker Test'),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete),
            onPressed: () => setState(() => _statusLog = ""),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // Configuration Card
            Card(
              elevation: 4,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    TextField(
                      controller: _urlController,
                      decoration: const InputDecoration(
                        labelText: 'API Endpoint / Socket URL',
                        border: OutlineInputBorder(),
                        prefixIcon: Icon(Icons.link),
                      ),
                    ),
                    const SizedBox(height: 10),
                    Row(
                      children: [
                        Expanded(
                          child: TextField(
                            controller: _userIdController,
                            decoration: const InputDecoration(
                              labelText: 'User ID (Extra Data)',
                              border: OutlineInputBorder(),
                              prefixIcon: Icon(Icons.person),
                            ),
                          ),
                        ),
                        const SizedBox(width: 10),
                        Column(
                          children: [
                            const Text("WebSocket"),
                            Switch(
                              value: _isWebSocket,
                              onChanged: (val) {
                                setState(() {
                                  _isWebSocket = val;
                                  // Auto-switch protocol prefix for convenience
                                  if (val &&
                                      _urlController.text.startsWith("http")) {
                                    _urlController.text = _urlController.text
                                        .replaceFirst("http", "ws");
                                  } else if (!val &&
                                      _urlController.text.startsWith("ws")) {
                                    _urlController.text = _urlController.text
                                        .replaceFirst("ws", "http");
                                  }
                                });
                              },
                            ),
                          ],
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 20),

            // Action Button
            SizedBox(
              width: double.infinity,
              height: 50,
              child: ElevatedButton.icon(
                style: ElevatedButton.styleFrom(
                  backgroundColor: _isTracking ? Colors.red : Colors.green,
                  foregroundColor: Colors.white,
                ),
                onPressed: _toggleTracking,
                icon: Icon(_isTracking ? Icons.stop : Icons.play_arrow),
                label: Text(
                  _isTracking ? "STOP TRACKING" : "START TRACKING",
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
              ),
            ),

            if (_isTracking) ...[
              const SizedBox(height: 10),
              SizedBox(
                width: double.infinity,
                height: 50,
                child: OutlinedButton.icon(
                  onPressed: _checkout,
                  icon: const Icon(Icons.check_circle_outline),
                  label: const Text(
                    "CHECKOUT (FLUSH DATA)",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),
              ),
            ],

            const SizedBox(height: 20),
            const Align(
              alignment: Alignment.centerLeft,
              child: Text(
                "Event Log:",
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
            ),
            const Divider(),

            // Log output
            Expanded(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.grey[200],
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.grey[400]!),
                ),
                child: SingleChildScrollView(
                  child: Text(
                    _statusLog,
                    style: const TextStyle(
                      fontFamily: 'Monospace',
                      fontSize: 12,
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
4
likes
0
points
501
downloads

Publisher

unverified uploader

Weekly Downloads

A battery-efficient adaptive location tracker with offline sync, Kalman filtering, and foreground service support.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on adaptive_location_tracker

Packages that implement adaptive_location_tracker