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,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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user