Implement Google Navigation Flutter integration for turn-by-turn delivery navigation
Adds complete Google Navigation support with: - LocationPermissionService for runtime location permissions - NavigationSessionService for session and route management - NavigationPage for full-screen turn-by-turn navigation UI - NavigationTermsAndConditionsDialog for service acceptance - Comprehensive i18n support (English/French) - Android minSdk=23 with Java NIO desugaring - iOS location permissions in Info.plist - Error handling with user-friendly dialogs - Location update and arrival notifications Includes detailed setup guide and implementation documentation with API key configuration instructions, integration examples, and testing checklist. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class NavigationTermsAndConditionsDialog extends StatelessWidget {
|
||||
final VoidCallback onAccept;
|
||||
final VoidCallback? onDecline;
|
||||
|
||||
const NavigationTermsAndConditionsDialog({
|
||||
Key? key,
|
||||
required this.onAccept,
|
||||
this.onDecline,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
l10n?.navigationTcTitle ?? 'Navigation Service',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n?.navigationTcDescription ??
|
||||
'This app uses Google Navigation to provide turn-by-turn navigation for deliveries.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n?.navigationTcAttribution ??
|
||||
'Attribution: Maps and navigation services provided by Google Maps.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n?.navigationTcTerms ??
|
||||
'By accepting, you agree to Google\'s Terms of Service and Privacy Policy for Navigation services.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (onDecline != null)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onDecline!();
|
||||
},
|
||||
child: Text(
|
||||
l10n?.decline ?? 'Decline',
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onAccept();
|
||||
},
|
||||
child: Text(
|
||||
l10n?.accept ?? 'Accept',
|
||||
style: TextStyle(color: colorScheme.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -65,5 +65,23 @@
|
||||
"completed": {"type": "int"},
|
||||
"total": {"type": "int"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"navigationTcTitle": "Navigation Service",
|
||||
"navigationTcDescription": "This app uses Google Navigation to provide turn-by-turn navigation for deliveries.",
|
||||
"navigationTcAttribution": "Attribution: Maps and navigation services provided by Google Maps.",
|
||||
"navigationTcTerms": "By accepting, you agree to Google's Terms of Service and Privacy Policy for Navigation services.",
|
||||
"accept": "Accept",
|
||||
"decline": "Decline",
|
||||
"locationPermissionRequired": "Location Permission",
|
||||
"locationPermissionMessage": "This app requires location permission to navigate to deliveries.",
|
||||
"locationPermissionDenied": "Location permission denied. Navigation cannot proceed.",
|
||||
"permissionPermanentlyDenied": "Permission Required",
|
||||
"openSettingsMessage": "Location permission is permanently denied. Please enable it in app settings.",
|
||||
"openSettings": "Open Settings",
|
||||
"cancel": "Cancel",
|
||||
"ok": "OK",
|
||||
"requestPermission": "Request Permission",
|
||||
"navigationArrived": "You have arrived at the destination",
|
||||
"navigatingTo": "Navigating to",
|
||||
"initializingNavigation": "Initializing navigation..."
|
||||
}
|
||||
|
||||
+19
-1
@@ -65,5 +65,23 @@
|
||||
"completed": {"type": "int"},
|
||||
"total": {"type": "int"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"navigationTcTitle": "Service de Navigation",
|
||||
"navigationTcDescription": "Cette application utilise Google Navigation pour fournir une navigation virage par virage pour les livraisons.",
|
||||
"navigationTcAttribution": "Attribution: Services de cartes et de navigation fournis par Google Maps.",
|
||||
"navigationTcTerms": "En acceptant, vous acceptez les conditions d'utilisation et la politique de confidentialit de Google pour les services de navigation.",
|
||||
"accept": "Accepter",
|
||||
"decline": "Refuser",
|
||||
"locationPermissionRequired": "Permission de localisation",
|
||||
"locationPermissionMessage": "Cette application ncessite la permission de localisation pour naviguer vers les livraisons.",
|
||||
"locationPermissionDenied": "Permission de localisation refuse. La navigation ne peut pas continuer.",
|
||||
"permissionPermanentlyDenied": "Permission requise",
|
||||
"openSettingsMessage": "La permission de localisation est dfinitivement refuse. Veuillez l'activer dans les paramtres de l'application.",
|
||||
"openSettings": "Ouvrir les paramtres",
|
||||
"cancel": "Annuler",
|
||||
"ok": "OK",
|
||||
"requestPermission": "Demander la permission",
|
||||
"navigationArrived": "Vous tes arriv la destination",
|
||||
"navigatingTo": "Navigation vers",
|
||||
"initializingNavigation": "Initialisation de la navigation..."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../models/delivery.dart';
|
||||
import '../services/location_permission_service.dart';
|
||||
import '../components/navigation_tc_dialog.dart';
|
||||
|
||||
class NavigationPage extends ConsumerStatefulWidget {
|
||||
final Delivery delivery;
|
||||
final double destinationLatitude;
|
||||
final double destinationLongitude;
|
||||
final VoidCallback? onNavigationComplete;
|
||||
final VoidCallback? onNavigationCancelled;
|
||||
|
||||
const NavigationPage({
|
||||
super.key,
|
||||
required this.delivery,
|
||||
required this.destinationLatitude,
|
||||
required this.destinationLongitude,
|
||||
this.onNavigationComplete,
|
||||
this.onNavigationCancelled,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<NavigationPage> createState() => _NavigationPageState();
|
||||
}
|
||||
|
||||
class _NavigationPageState extends ConsumerState<NavigationPage> {
|
||||
late GoogleMapsNavigationViewController _navigationViewController;
|
||||
late LocationPermissionService _permissionService;
|
||||
bool _isNavigationInitialized = false;
|
||||
bool _hasLocationPermission = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_permissionService = LocationPermissionService();
|
||||
_initializeNavigation();
|
||||
}
|
||||
|
||||
Future<void> _initializeNavigation() async {
|
||||
try {
|
||||
final hasPermission = await _permissionService.hasLocationPermission();
|
||||
|
||||
if (!hasPermission) {
|
||||
if (mounted) {
|
||||
_showPermissionDialog();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_hasLocationPermission = true;
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
_initializeNavigationSession();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
_showErrorDialog('Initialization error: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeNavigationSession() async {
|
||||
try {
|
||||
await GoogleMapsNavigationViewController.initializeNavigationSession();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigationInitialized = true;
|
||||
});
|
||||
|
||||
// Set destination after session is initialized
|
||||
await _setDestination();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
_showErrorDialog('Failed to initialize navigation: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setDestination() async {
|
||||
try {
|
||||
final destination = NavigationDisplayOptions(
|
||||
showDestinationMarkers: true,
|
||||
);
|
||||
|
||||
final waypoint = Waypoint(
|
||||
title: widget.delivery.name,
|
||||
target: LatLng(
|
||||
latitude: widget.destinationLatitude,
|
||||
longitude: widget.destinationLongitude,
|
||||
),
|
||||
);
|
||||
|
||||
await _navigationViewController.setDestinations(
|
||||
destinations: [waypoint],
|
||||
displayOptions: destination,
|
||||
);
|
||||
|
||||
// Listen for location updates
|
||||
_navigationViewController.addOnLocationUpdatedListener((location) {
|
||||
debugPrint(
|
||||
'Location updated: ${location.latitude}, ${location.longitude}',
|
||||
);
|
||||
});
|
||||
|
||||
// Listen for navigation events
|
||||
_navigationViewController.addOnNavigationUIEnabledListener((isEnabled) {
|
||||
debugPrint('Navigation UI enabled: $isEnabled');
|
||||
});
|
||||
|
||||
// Listen for waypoint reached
|
||||
_navigationViewController.addOnArrivalListener((arrival) {
|
||||
_handleArrival(arrival);
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
_showErrorDialog('Failed to set destination: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleArrival(NavInfo navInfo) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)?.navigationArrived ??
|
||||
'You have arrived at the destination',
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
|
||||
// Call completion callback
|
||||
widget.onNavigationComplete?.call();
|
||||
}
|
||||
}
|
||||
|
||||
void _showPermissionDialog() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n?.locationPermissionRequired ?? 'Location Permission'),
|
||||
content: Text(
|
||||
l10n?.locationPermissionMessage ??
|
||||
'This app requires location permission to navigate to deliveries.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onNavigationCancelled?.call();
|
||||
},
|
||||
child: Text(l10n?.cancel ?? 'Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_requestLocationPermission();
|
||||
},
|
||||
child: Text(l10n?.requestPermission ?? 'Request Permission'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _requestLocationPermission() async {
|
||||
final result = await _permissionService.requestLocationPermission();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
result.when(
|
||||
granted: () {
|
||||
setState(() {
|
||||
_hasLocationPermission = true;
|
||||
});
|
||||
_initializeNavigationSession();
|
||||
},
|
||||
denied: () {
|
||||
_showErrorDialog(
|
||||
AppLocalizations.of(context)?.locationPermissionDenied ??
|
||||
'Location permission denied. Navigation cannot proceed.',
|
||||
);
|
||||
widget.onNavigationCancelled?.call();
|
||||
},
|
||||
permanentlyDenied: () {
|
||||
_showPermissionSettingsDialog();
|
||||
},
|
||||
error: (message) {
|
||||
_showErrorDialog(message);
|
||||
widget.onNavigationCancelled?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showPermissionSettingsDialog() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n?.permissionPermanentlyDenied ?? 'Permission Required'),
|
||||
content: Text(
|
||||
l10n?.openSettingsMessage ??
|
||||
'Location permission is permanently denied. Please enable it in app settings.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(l10n?.cancel ?? 'Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_permissionService.openAppSettings();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(l10n?.openSettings ?? 'Open Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorDialog(String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)?.error ?? 'Error'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onNavigationCancelled?.call();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)?.ok ?? 'OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'${l10n?.navigatingTo ?? 'Navigating to'}: ${widget.delivery.name}',
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
body: _hasLocationPermission && _isNavigationInitialized
|
||||
? GoogleMapsNavigationView(
|
||||
onViewCreated: (controller) {
|
||||
_navigationViewController = controller;
|
||||
},
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(
|
||||
latitude: widget.destinationLatitude,
|
||||
longitude: widget.destinationLongitude,
|
||||
),
|
||||
zoom: 15,
|
||||
),
|
||||
useMarkerClusteringForDynamicMarkers: true,
|
||||
zoomGesturesEnabled: true,
|
||||
scrollGesturesEnabled: true,
|
||||
navigationUIEnabled: true,
|
||||
mapToolbarEnabled: true,
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n?.initializingNavigation ??
|
||||
'Initializing navigation...',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: _hasLocationPermission && _isNavigationInitialized
|
||||
? FloatingActionButton(
|
||||
onPressed: () {
|
||||
widget.onNavigationCancelled?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Icon(Icons.close),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class LocationPermissionService {
|
||||
static const String _tcKey = 'navigation_tc_accepted';
|
||||
|
||||
Future<LocationPermissionResult> requestLocationPermission() async {
|
||||
final status = await Permission.location.request();
|
||||
|
||||
return switch (status) {
|
||||
PermissionStatus.granted => LocationPermissionResult.granted(),
|
||||
PermissionStatus.denied => LocationPermissionResult.denied(),
|
||||
PermissionStatus.permanentlyDenied =>
|
||||
LocationPermissionResult.permanentlyDenied(),
|
||||
_ => LocationPermissionResult.error(
|
||||
message: 'Unexpected permission status: $status',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Future<bool> hasLocationPermission() async {
|
||||
final status = await Permission.location.status;
|
||||
return status.isGranted;
|
||||
}
|
||||
|
||||
Future<void> openAppSettings() async {
|
||||
await openAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LocationPermissionResult {
|
||||
const LocationPermissionResult();
|
||||
|
||||
factory LocationPermissionResult.granted() => _Granted();
|
||||
factory LocationPermissionResult.denied() => _Denied();
|
||||
factory LocationPermissionResult.permanentlyDenied() =>
|
||||
_PermanentlyDenied();
|
||||
factory LocationPermissionResult.error({required String message}) =>
|
||||
_Error(message);
|
||||
|
||||
R when<R>({
|
||||
required R Function() granted,
|
||||
required R Function() denied,
|
||||
required R Function() permanentlyDenied,
|
||||
required R Function(String message) error,
|
||||
}) {
|
||||
return switch (this) {
|
||||
_Granted() => granted(),
|
||||
_Denied() => denied(),
|
||||
_PermanentlyDenied() => permanentlyDenied(),
|
||||
_Error(:final message) => error(message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final class _Granted extends LocationPermissionResult {
|
||||
const _Granted();
|
||||
}
|
||||
|
||||
final class _Denied extends LocationPermissionResult {
|
||||
const _Denied();
|
||||
}
|
||||
|
||||
final class _PermanentlyDenied extends LocationPermissionResult {
|
||||
const _PermanentlyDenied();
|
||||
}
|
||||
|
||||
final class _Error extends LocationPermissionResult {
|
||||
final String message;
|
||||
const _Error(this.message);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
|
||||
|
||||
class NavigationSessionService {
|
||||
static final NavigationSessionService _instance =
|
||||
NavigationSessionService._internal();
|
||||
|
||||
factory NavigationSessionService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
NavigationSessionService._internal();
|
||||
|
||||
bool _isSessionInitialized = false;
|
||||
GoogleMapsNavigationViewController? _controller;
|
||||
|
||||
bool get isSessionInitialized => _isSessionInitialized;
|
||||
|
||||
Future<void> initializeSession() async {
|
||||
if (_isSessionInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await GoogleMapsNavigationViewController.initializeNavigationSession();
|
||||
_isSessionInitialized = true;
|
||||
} catch (e) {
|
||||
throw NavigationSessionException('Failed to initialize session: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setController(
|
||||
GoogleMapsNavigationViewController controller,
|
||||
) async {
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
Future<NavigationRoute> calculateRoute({
|
||||
required double startLatitude,
|
||||
required double startLongitude,
|
||||
required double destinationLatitude,
|
||||
required double destinationLongitude,
|
||||
}) async {
|
||||
if (!_isSessionInitialized) {
|
||||
throw NavigationSessionException('Session not initialized');
|
||||
}
|
||||
|
||||
if (_controller == null) {
|
||||
throw NavigationSessionException('Controller not set');
|
||||
}
|
||||
|
||||
try {
|
||||
final origin = LatLng(
|
||||
latitude: startLatitude,
|
||||
longitude: startLongitude,
|
||||
);
|
||||
|
||||
final destination = LatLng(
|
||||
latitude: destinationLatitude,
|
||||
longitude: destinationLongitude,
|
||||
);
|
||||
|
||||
final waypoint = Waypoint(
|
||||
target: destination,
|
||||
);
|
||||
|
||||
// Set destinations will trigger route calculation
|
||||
await _controller!.setDestinations(
|
||||
destinations: [waypoint],
|
||||
);
|
||||
|
||||
return NavigationRoute(
|
||||
startLocation: origin,
|
||||
endLocation: destination,
|
||||
isCalculated: true,
|
||||
);
|
||||
} catch (e) {
|
||||
throw NavigationSessionException('Failed to calculate route: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startNavigation() async {
|
||||
if (!_isSessionInitialized || _controller == null) {
|
||||
throw NavigationSessionException('Navigation not properly initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
await _controller!.startGuidance();
|
||||
} catch (e) {
|
||||
throw NavigationSessionException('Failed to start navigation: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopNavigation() async {
|
||||
if (_controller == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _controller!.stopGuidance();
|
||||
} catch (e) {
|
||||
throw NavigationSessionException('Failed to stop navigation: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void addLocationListener(
|
||||
Function(LatLng location) onLocationUpdate,
|
||||
) {
|
||||
if (_controller == null) {
|
||||
throw NavigationSessionException('Controller not set');
|
||||
}
|
||||
|
||||
_controller!.addOnLocationUpdatedListener((location) {
|
||||
onLocationUpdate(location);
|
||||
});
|
||||
}
|
||||
|
||||
void addArrivalListener(Function(NavInfo info) onArrival) {
|
||||
if (_controller == null) {
|
||||
throw NavigationSessionException('Controller not set');
|
||||
}
|
||||
|
||||
_controller!.addOnArrivalListener((info) {
|
||||
onArrival(info);
|
||||
});
|
||||
}
|
||||
|
||||
void addRemainingDistanceListener(
|
||||
Function(int distanceMeters) onDistanceChange,
|
||||
) {
|
||||
if (_controller == null) {
|
||||
throw NavigationSessionException('Controller not set');
|
||||
}
|
||||
|
||||
_controller!.addOnRemainingDistanceChangedListener((distance) {
|
||||
onDistanceChange(distance);
|
||||
});
|
||||
}
|
||||
|
||||
void clearAllListeners() {
|
||||
if (_controller == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_controller!.removeAllListeners();
|
||||
}
|
||||
|
||||
Future<void> cleanup() async {
|
||||
try {
|
||||
if (_controller != null) {
|
||||
await stopNavigation();
|
||||
clearAllListeners();
|
||||
}
|
||||
_isSessionInitialized = false;
|
||||
_controller = null;
|
||||
} catch (e) {
|
||||
throw NavigationSessionException('Failed to cleanup: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationRoute {
|
||||
final LatLng startLocation;
|
||||
final LatLng endLocation;
|
||||
final bool isCalculated;
|
||||
final int? distanceMeters;
|
||||
final Duration? estimatedTime;
|
||||
|
||||
NavigationRoute({
|
||||
required this.startLocation,
|
||||
required this.endLocation,
|
||||
required this.isCalculated,
|
||||
this.distanceMeters,
|
||||
this.estimatedTime,
|
||||
});
|
||||
}
|
||||
|
||||
class NavigationSessionException implements Exception {
|
||||
final String message;
|
||||
|
||||
NavigationSessionException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'NavigationSessionException: $message';
|
||||
}
|
||||
Reference in New Issue
Block a user