Restructures navigation session initialization to occur after the view is created, eliminating race conditions. Session initialization now happens in onViewCreated callback with proper delay before setting destination. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
303 lines
8.7 KiB
Dart
303 lines
8.7 KiB
Dart
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;
|
|
_isNavigationInitialized = true;
|
|
});
|
|
} catch (e) {
|
|
if (mounted) {
|
|
_showErrorDialog('Initialization error: ${e.toString()}');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _initializeNavigationSession() async {
|
|
try {
|
|
await GoogleMapsNavigationViewController.initializeNavigationSession();
|
|
} catch (e) {
|
|
debugPrint('Navigation session initialization error: $e');
|
|
// Don't show error dialog, just log it
|
|
// The session might already be initialized
|
|
}
|
|
}
|
|
|
|
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) async {
|
|
_navigationViewController = controller;
|
|
await _initializeNavigationSession();
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
await _setDestination();
|
|
},
|
|
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();
|
|
}
|
|
}
|