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:
Jean-Philippe Brule
2025-11-15 20:43:29 -05:00
parent 5714fd8443
commit 46af8f55a2
10 changed files with 1312 additions and 3 deletions
@@ -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';
}