vietmap_tracking_plugin 1.0.5 copy "vietmap_tracking_plugin: ^1.0.5" to clipboard
vietmap_tracking_plugin: ^1.0.5 copied to clipboard

A comprehensive Flutter plugin for GPS tracking and location data transmission to Vietmap's tracking API with background service support.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'tracking_provider.dart';
import 'widgets/cache_card.dart';
import 'widgets/config_card.dart';
import 'widgets/controls_card.dart';
import 'widgets/fake_gps_card.dart';
import 'widgets/header_card.dart';
import 'widgets/location_card.dart';
import 'widgets/location_history_card.dart';
import 'widgets/permission_section.dart';
import 'widgets/session_stats_card.dart';
import 'widgets/smart_battery_card.dart';
import 'widgets/speed_alert_card.dart';
import 'widgets/tracking_status_card.dart';
import 'widgets/user_identity_card.dart';

// SLC support is temporarily disabled until the SDK exposes the updated iOS API.
// const slcChannel = MethodChannel('vietmap_tracking_plugin/slc');

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  debugPrint('=======Example App Bootstrap=======');
  final provider = TrackingProvider();
  await provider.initNotifications();
  runApp(
    ChangeNotifierProvider.value(
      value: provider,
      child: const MyApp(),
    ),
  );
  debugPrint('=======End Example App Bootstrap=======');
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Vietmap Tracking Demo',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const TrackingDemoPage(),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Page — stateful only for TextEditingControllers + email-edit toggle
// ─────────────────────────────────────────────────────────────────────────────

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

  @override
  State<TrackingDemoPage> createState() => _TrackingDemoPageState();
}

class _TrackingDemoPageState extends State<TrackingDemoPage> {
  final _emailController = TextEditingController();
  final _customIntervalController = TextEditingController(text: '5000');
  final _customDistanceController = TextEditingController(text: '10');
  final _maxRecordsController = TextEditingController(text: '5000');
  final _maxDbSizeMbController = TextEditingController(text: '50');
  final _batchSizeController = TextEditingController(text: '50');

  bool _emailEditing = false;

  @override
  void initState() {
    super.initState();
    debugPrint('=======Bind Tracking Demo Page=======');
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      final p = context.read<TrackingProvider>();
      await p.init();
      // Initialize SDK automatically — replace 'your-api-key' with your key.
      // Contact Vietmap to get an API key.
      await p.configureSdk(
        'your-api-key', // Required: replace with your actual API key
        baseURL: 'your-tracking-server.com/api/v1', // Optional: only if using a custom server
      );
      _emailController.text = p.userEmail;
      // SLC hooks are temporarily disabled.
      // await p.checkSLCWakeUp(slcChannel);
      // p.listenSLCEvents(slcChannel);
      debugPrint('=======End Bind Tracking Demo Page=======');
    });
  }

  @override
  void dispose() {
    _emailController.dispose();
    _customIntervalController.dispose();
    _customDistanceController.dispose();
    _maxRecordsController.dispose();
    _maxDbSizeMbController.dispose();
    _batchSizeController.dispose();
    super.dispose();
  }

  void _snack(String msg, {Color bg = Colors.blue}) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(msg), backgroundColor: bg),
    );
  }

  Future<void> _handleRequestPermissions() async {
    try {
      final r = await context.read<TrackingProvider>().requestPermissions();
      _snack(
        r.granted ? '✅ Location permissions granted' : '❌ Permissions denied',
        bg: r.granted ? Colors.green : Colors.red,
      );
    } catch (e) {
      _snack('❌ Error: $e', bg: Colors.red);
    }
  }

  Future<void> _startTracking() async {
    final p = context.read<TrackingProvider>();
    if (p.isStartingTracking || p.isStoppingTracking) return;
    if (!p.isSdkConfigured) {
      _snack('⚠️ Initialize SDK first', bg: Colors.orange);
      return;
    }
    if (!p.hasPermissions) {
      _snack('⚠️ Location permissions required', bg: Colors.orange);
      return;
    }
    try {
      final ok = await p.startTracking();
      _snack(ok ? '✅ Tracking started' : '⚠️ Could not start tracking',
          bg: ok ? Colors.green : Colors.orange);
    } catch (e) {
      _snack('❌ Failed: $e', bg: Colors.red);
    }
  }

  Future<void> _stopTracking() async {
    final p = context.read<TrackingProvider>();
    if (p.isStartingTracking || p.isStoppingTracking) return;
    try {
      final ok = await p.stopTracking();
      _snack(ok ? '✅ Tracking stopped' : '⚠️ Stop result unclear', bg: Colors.orange);
    } catch (e) {
      _snack('❌ Error: $e', bg: Colors.red);
    }
  }

  Future<void> _handleUpdateConfig() async {
    final p = context.read<TrackingProvider>();
    if (!p.isTracking) {
      _snack('⚠️ Start tracking first', bg: Colors.orange);
      return;
    }
    try {
      final ok = await p.updateConfig();
      if (ok) _snack('✅ Configuration updated', bg: Colors.green);
    } catch (e) {
      _snack('❌ Failed to update config', bg: Colors.red);
    }
  }

  Future<void> _getCurrentLocation() async {
    final p = context.read<TrackingProvider>();
    if (!p.hasPermissions) {
      _snack('⚠️ Permissions not granted', bg: Colors.orange);
      return;
    }
    try {
      final loc = await p.getCurrentLocation();
      _snack('📍 ${loc.latitude.toStringAsFixed(6)}, ${loc.longitude.toStringAsFixed(6)}',
          bg: Colors.blue);
    } catch (e) {
      _snack('❌ $e', bg: Colors.red);
    }
  }

  Future<void> _refreshStatus() async {
    await context.read<TrackingProvider>().refreshStatus();
    _snack('🔄 Status refreshed', bg: Colors.blue);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('🛰️ GPS Tracking Demo'), elevation: 2),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const HeaderCard(),
            const SizedBox(height: 16),
            PermissionSection(onRequest: _handleRequestPermissions),
            const SizedBox(height: 16),
                        ControlsCard(
              onStart: _startTracking,
              onStop: _stopTracking,
              onGetLocation: _getCurrentLocation,
              onUpdateConfig: _handleUpdateConfig,
              onRefreshStatus: _refreshStatus,
              onClearHistory: () => context.read<TrackingProvider>().clearHistory(),
            ),
            const LocationHistoryCard(),
            // SLC UI is temporarily disabled until native support is updated.
            // if (Platform.isIOS) _SLCCard(),
            const SizedBox(height: 16),
            UserIdentityCard(
              emailController: _emailController,
              isEditing: _emailEditing,
              onToggleEdit: () => setState(() => _emailEditing = !_emailEditing),
              onSave: () async {
                await context.read<TrackingProvider>().saveEmail(_emailController.text);
                setState(() => _emailEditing = false);
                _snack('✅ Email saved', bg: Colors.green);
              },
            ),
            const SizedBox(height: 16),

            const SpeedAlertCard(),
            const SizedBox(height: 16),
            ConfigCard(
              intervalController: _customIntervalController,
              distanceController: _customDistanceController,
            ),
            const SizedBox(height: 16),
            const SessionStatsCard(),
            const SizedBox(height: 16),
            const TrackingStatusCard(),
            const SizedBox(height: 16),
            const LocationCard(),
            const SizedBox(height: 16),
            const FakeGpsCard(),
            const SizedBox(height: 16),
            const SmartBatteryCard(),
            const SizedBox(height: 16),
            CacheCard(
              maxRecordsController: _maxRecordsController,
              maxDbSizeMbController: _maxDbSizeMbController,
              batchSizeController: _batchSizeController,
            ),
          ],
        ),
      ),
    );
  }
}