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