This document describes how to implement route navigation and navigation simulation functionality in a Flutter mapping application using the Magic Lane SDK. The implementation provides both real GPS-based navigation and simulated navigation for testing and demonstration purposes.
- Architecture
- Core Components
- Data Formatting Utilities
- Integration Requirements
- Usage Workflow
- Key Differences: Navigation vs Simulation
- Error Handling
- Conclusion
The navigation system consists of several key components:
lib/navigation/
├── navigation_engine.dart # Core navigation service
├── navigation_top_panel.dart # Turn-by-turn instruction panel
└── navigation_bottom_panel.dart # Trip progress panel
lib/providers/
└── navigation_state_provider.dart # Navigation state management
The navigation system integrates with:
- Routing System - For route calculation and display
- Map Controller - For GPS following and map updates
- Position Service - For real-time location tracking
- Magic Lane NavigationService - For turn-by-turn navigation
Manages the navigation state throughout the application lifecycle.
import 'package:flutter/material.dart';
import 'package:magiclane_maps_flutter/magiclane_maps_flutter.dart';
// Provider for managing the navigation state throughout the app
class NavigationStateProvider extends ChangeNotifier {
bool _isNavigating = false;
NavigationInstruction? _currentInstruction;
TaskHandler? _navigationHandler;
bool get isNavigating => _isNavigating;
NavigationInstruction? get currentInstruction => _currentInstruction;
TaskHandler? get navigationHandler => _navigationHandler;
set currentInstruction(NavigationInstruction? instruction) {
_currentInstruction = instruction;
notifyListeners();
}
void startNavigation(TaskHandler navigationHandler) {
if (!_isNavigating) {
_isNavigating = true;
_navigationHandler = navigationHandler;
notifyListeners();
}
}
void stopNavigation() {
if (_isNavigating) {
_isNavigating = false;
_navigationHandler = null;
notifyListeners();
}
}
}Key Features:
- Navigation State Tracking - Manages active navigation status
- Instruction Management - Stores current turn-by-turn instructions
- Handler Management - Maintains reference to navigation task handler
- State Notifications - Notifies UI of navigation state changes
Core service that handles navigation and simulation operations.
import 'package:flutter/material.dart' hide Route; // Avoid name conflict with Magic Lane SDK Route class
import 'package:provider/provider.dart' as provider;
class NavigationEngine {
/// Internal helper method that safely starts navigation or simulation
static void _startNavigation(
BuildContext context,
TaskHandler? Function(
Route route, {
required void Function(NavigationInstruction, dynamic) onNavigationInstruction,
required void Function(GemError) onError,
void Function(Landmark)? onDestinationReached,
})
navigationStarter,
) {
// Capture all necessary providers up front
final gemMapProvider = provider.Provider.of<GemMapProvider>(context, listen: false);
final navigationStateProvider = provider.Provider.of<NavigationStateProvider>(context, listen: false);
final controller = gemMapProvider.controller;
if (controller == null) return;
final routes = controller.preferences.routes;
if (routes.mainRoute == null) {
ErrorService.showWarning('No main route available');
return;
}
// Clear any existing routes on map except the main route and remove its label
controller.preferences.routes.clearAllButMainRoute();
controller.preferences.routes.hideLabel(routes.mainRoute!);
void errorHandler(GemError error) {
if (error == GemError.cancel) return;
navigationStateProvider.stopNavigation();
NavigationService.cancelNavigation(navigationStateProvider.navigationHandler);
gemMapProvider.controller?.stopFollowingPosition();
gemMapProvider.controller?.preferences.routes.clear();
ErrorService.handleGemError(error, context: 'Navigation');
}
void destinationReachedHandler(Landmark landmark) {
navigationStateProvider.stopNavigation();
gemMapProvider.controller?.stopFollowingPosition();
gemMapProvider.controller?.preferences.routes.clear();
gemMapProvider.controller?.deactivateAllHighlights();
ErrorService.showSuccess('Destination reached: ${landmark.name}');
}
// Start navigation
final navigationHandler = navigationStarter(
routes.mainRoute!,
onNavigationInstruction: (instruction, events) {
navigationStateProvider.currentInstruction = instruction;
},
onError: errorHandler,
onDestinationReached: destinationReachedHandler,
);
if (navigationHandler == null) {
ErrorService.handleError('Failed to start navigation', context: 'Navigation');
return;
}
navigationStateProvider.startNavigation(navigationHandler);
controller.startFollowingPosition();
}
static void startNavigation(BuildContext context) {
_startNavigation(context, (route, {required onNavigationInstruction, required onError, onDestinationReached}) {
return NavigationService.startNavigation(
route,
onNavigationInstruction: onNavigationInstruction,
onError: onError,
onDestinationReached: onDestinationReached,
);
});
}
static void startSimulation(BuildContext context) {
_startNavigation(context, (route, {required onNavigationInstruction, required onError, onDestinationReached}) {
return NavigationService.startSimulation(
route,
onNavigationInstruction: onNavigationInstruction,
onError: onError,
onDestinationReached: onDestinationReached,
);
});
}
static void stopNavigation(BuildContext context) {
// Stop navigation
final navigationStateProvider = provider.Provider.of<NavigationStateProvider>(context, listen: false);
navigationStateProvider.stopNavigation();
NavigationService.cancelNavigation(navigationStateProvider.navigationHandler);
// Reset map state
final gemMapProvider = provider.Provider.of<GemMapProvider>(context, listen: false);
gemMapProvider.controller?.stopFollowingPosition();
gemMapProvider.controller?.deactivateAllHighlights();
gemMapProvider.controller?.preferences.routes.clear();
}
}Key Features:
- Unified Navigation Interface - Single API for both navigation and simulation
- State Management Integration - Seamless integration with navigation state provider
- Error Handling - Comprehensive error handling with user feedback
- Map Integration - Automatic GPS following and route display
- Resource Cleanup - Proper cleanup when navigation ends
lib/navigation/navigation_top_panel.dart
import 'package:magiclane_maps_flutter/core.dart';
import 'package:magiclane_maps_flutter/navigation.dart';
import 'package:flutter/material.dart';
class TopNavigationPanel extends StatelessWidget {
// The current navigation instruction to display
final NavigationInstruction instruction;
const TopNavigationPanel({super.key, required this.instruction});
@override
Widget build(BuildContext context) {
final topInset = MediaQuery.of(context).padding.top;
final panelHeight = MediaQuery.of(context).size.height * 0.15 + topInset;
return Container(
width: MediaQuery.of(context).size.width,
height: panelHeight,
padding: EdgeInsets.only(top: topInset, left: 10, right: 10, bottom: 10),
decoration: const BoxDecoration(color: Colors.black),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: instruction.nextTurnDetails != null &&
instruction.nextTurnDetails!.abstractGeometryImg.isValid
? Image.memory(
instruction.nextTurnDetails!.abstractGeometryImg.getRenderableImageBytes(
size: Size(200, 200),
format: ImageFileFormat.png,
)!,
gaplessPlayback: true,
)
: const SizedBox(), // Empty widget
),
SizedBox(
width: MediaQuery.of(context).size.width - 150,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
FormatUtils.getFormattedDistanceToNextTurn(instruction),
textAlign: TextAlign.left,
style: const TextStyle(
color: Colors.white,
fontSize: 25,
fontWeight: FontWeight.w600
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Text(
instruction.nextStreetName,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}Key Features:
- Turn Visual - Displays turn geometry image if available
- Distance Display - Shows formatted distance to next turn
- Street Information - Displays next street name
- Responsive Design - Adapts to different screen sizes
- Safe Area - Accounts for device notches and status bars
lib/navigation/navigation_bottom_panel.dart
import 'package:flutter/material.dart';
// Bottom panel widget displaying navigation details such as remaining duration, distance, and ETA.
class NavigationBottomPanel extends StatelessWidget {
final String remainingDuration;
final String remainingDistance;
final String eta;
const NavigationBottomPanel({
super.key,
required this.remainingDuration,
required this.remainingDistance,
required this.eta,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
),
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
remainingDuration,
style: const TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.w500
),
),
Text(
eta,
style: const TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.w500
),
),
Text(
remainingDistance,
style: const TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.w500
),
),
],
),
);
}
}Add the following _TopNavigationWidget class:
lib/widgets/map_overlay_manager.dart
class _TopNavigationWidget extends StatelessWidget {
const _TopNavigationWidget();
@override
Widget build(BuildContext context) {
return Selector<NavigationStateProvider, NavigationInstruction?>(
selector: (context, provider) => provider.isNavigating ? provider.currentInstruction : null,
builder: (context, instruction, child) {
if (instruction == null) return const SizedBox.shrink();
return Align(
alignment: Alignment.topCenter,
child: TopNavigationPanel(instruction: instruction),
);
},
);
}
}Key Features:
- Trip Summary - Shows remaining time, distance, and ETA
- Clean Design - White rounded container with shadow
- Readable Layout - Large, clear text with proper spacing
- Flexible Content - Accepts formatted strings for easy customization
The navigation UI is integrated through the map overlay manager which handles the display logic.
class NavigationControls extends StatelessWidget {
final NavigationInstruction instruction;
const NavigationControls({super.key, required this.instruction});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FloatingActionButton.extended(
heroTag: 'stopNavigation',
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
icon: const Icon(Icons.stop),
label: Text(AppLocalizations.of(context)!.stop),
onPressed: () => NavigationEngine.stopNavigation(context),
),
const FollowPositionButton(isInNavigationMode: true),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: NavigationBottomPanel(
remainingDuration: FormatUtils.getFormattedRemainingDuration(instruction),
remainingDistance: FormatUtils.getFormattedRemainingDistance(instruction),
eta: FormatUtils.getFormattedETA(instruction),
),
),
],
),
),
);
}
}lib/widgets/map_overlay_manager.dart (Top Panel Integration)
class MapOverlayManager extends StatelessWidget {
const MapOverlayManager({super.key});
@override
Widget build(BuildContext context) {
return const Stack(
children: [
// Bottom controls - only show when not in any specific mode
Positioned(bottom: 0, left: 0, right: 0, child: _BottomControlsWidget()),
// New top navigation panel
Positioned(top: 0, left: 0, right: 0, child: _TopNavigationWidget()),
// Dynamic bottom panel (landmark/routing/navigation)
Positioned(bottom: 0, left: 0, right: 0, child: _DynamicBottomPanel()),
],
);
}
}Key Features:
- Stop Navigation - Floating action button to end navigation
- Follow Position - GPS following toggle in navigation mode
- Progress Display - Real-time trip progress information
- Safe Area Layout - Proper positioning avoiding system UI
The route panel provides the entry point to start navigation or simulation.
Widget _buildActions(BuildContext context, Route route) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.near_me),
label: Text(AppLocalizations.of(context)!.navigate),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple[700],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12.0),
),
onPressed: () {
// First start navigation with current context
NavigationEngine.startNavigation(context);
// Then reset the routing state after navigation is started
// This will dispose this panel but navigation is already initialized
final routingStateProvider = context.read<RoutingStateProvider>();
routingStateProvider.reset();
},
),
),
SizedBox(width: 16.0),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.navigation_outlined),
label: Text(AppLocalizations.of(context)!.simulate),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple[700],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12.0),
),
onPressed: () {
// First start simulation with current context
NavigationEngine.startSimulation(context);
// Then reset the routing state after simulation is started
// This will dispose this panel but simulation is already initialized
final routingStateProvider = context.read<RoutingStateProvider>();
routingStateProvider.reset();
},
),
),
],
),
);
}lib/widgets/map_overlay_manager.dart (Panel State Update)
class _PanelState {
final Landmark? landmark;
final Route? route;
final bool isNavigating; // New navigation state flag
final NavigationInstruction? instruction; // Current navigation instruction
const _PanelState({this.landmark, this.route, required this.isNavigating, this.instruction});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _PanelState &&
runtimeType == other.runtimeType &&
landmark == other.landmark &&
route == other.route &&
isNavigating == other.isNavigating &&
instruction == other.instruction;
@override
int get hashCode => Object.hash(landmark, route, isNavigating, instruction);
}lib/widgets/map_overlay_manager.dart (Bottom Controls Update)
class _BottomControlsWidget extends StatelessWidget {
const _BottomControlsWidget();
@override
Widget build(BuildContext context) {
return Selector3<SelectedLandmarkStateProvider, RoutingStateProvider, NavigationStateProvider, bool>(
selector: (context, landmark, routing, navigation) =>
landmark.selected == null && routing.currentRoute == null && !navigation.isNavigating, // New navigation check added
builder: (context, shouldShow, child) {
if (!shouldShow) return const SizedBox.shrink();
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
children: [
const TransportModeButton(),
const SizedBox(width: 10),
const Expanded(child: SearchBarWidget()),
const SizedBox(width: 10),
const FollowPositionButton(),
],
),
],
),
),
),
);
},
);
}
}Key Features:
- Navigate Button - Starts real GPS-based navigation
- Simulate Button - Starts simulated navigation for testing
- State Management - Properly transitions from routing to navigation state
- Visual Design - Consistent styling with the rest of the app
The formatting utilities provide consistent display of navigation data throughout the UI.
// Utility functions to format various NavigationInstruction properties
static String getFormattedDistanceToNextTurn(NavigationInstruction navInstruction) {
final totalDistanceToTurn =
navInstruction.timeDistanceToNextTurn.unrestrictedDistanceM +
navInstruction.timeDistanceToNextTurn.restrictedDistanceM;
return convertDistance(totalDistanceToTurn);
}
static String getFormattedRemainingDistance(NavigationInstruction navInstruction) {
final remainingDistance =
navInstruction.remainingTravelTimeDistance.unrestrictedDistanceM +
navInstruction.remainingTravelTimeDistance.restrictedDistanceM;
return convertDistance(remainingDistance);
}
static String getFormattedRemainingDuration(NavigationInstruction navInstruction) {
final remainingDuration =
navInstruction.remainingTravelTimeDistance.unrestrictedTimeS +
navInstruction.remainingTravelTimeDistance.restrictedTimeS;
return convertDuration(remainingDuration);
}
static String getFormattedETA(NavigationInstruction navInstruction) {
final remainingDuration =
navInstruction.remainingTravelTimeDistance.unrestrictedTimeS +
navInstruction.remainingTravelTimeDistance.restrictedTimeS;
return getCurrentTime(additionalSeconds: remainingDuration);
}
// Utility function to add the given additional time to current time
static String getCurrentTime({int additionalHours = 0, int additionalMinutes = 0, int additionalSeconds = 0}) {
var now = DateTime.now();
var updatedTime = now.add(Duration(hours: additionalHours, minutes: additionalMinutes, seconds: additionalSeconds));
var formatter = DateFormat('HH:mm');
return formatter.format(updatedTime);
}Key Features:
- Distance Formatting - Converts meters to readable format (km/m)
- Time Formatting - Converts seconds to readable duration
- ETA Calculation - Provides estimated arrival time
- Restricted/Unrestricted - Handles both types of route segments
Add the navigation state provider to your app's provider tree:
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => GemMapProvider()),
ChangeNotifierProvider(create: (_) => FollowPositionProvider()),
ChangeNotifierProvider(create: (_) => SelectedLandmarkStateProvider()),
ChangeNotifierProvider(create: (_) => RoutingStateProvider()),
ChangeNotifierProvider(create: (_) => NavigationStateProvider()), // New provider added
],
child: child,
);Integrate navigation UI with your map overlay system:
class _DynamicBottomPanel extends StatelessWidget {
const _DynamicBottomPanel();
@override
Widget build(BuildContext context) {
return Selector3<SelectedLandmarkStateProvider, RoutingStateProvider, NavigationStateProvider, _PanelState>(
selector: (context, landmark, routing, navigation) => _PanelState(
landmark: landmark.selected,
route: routing.currentRoute,
isNavigating: navigation.isNavigating, // New navigation state
instruction: navigation.currentInstruction, // Current navigation instruction
),
builder: (context, state, child) {
return Align(alignment: Alignment.bottomCenter, child: _buildPanelForState(context, state));
},
);
}
Widget _buildPanelForState(BuildContext context, _PanelState state) {
if (state.landmark != null) {
return const LandmarkPanel();
}
if (state.route != null) {
return const RoutingPanel();
}
// Display navigation controls if navigating
if (state.isNavigating && state.instruction != null) {
return NavigationControls(instruction: state.instruction!);
}
return const SizedBox.shrink();
}
}Ensure your routing system can transition to navigation:
// In your routing panel or route display widget
ElevatedButton(
onPressed: () {
NavigationEngine.startNavigation(context); // or startSimulation(context)
routingStateProvider.reset(); // Clear routing state
},
child: Text('Navigate'),
)Users first calculate a route using the routing system:
- Select destination landmark
- Choose transport mode
- Calculate and display route
From the route panel, users can choose:
- Navigate - Real GPS-based navigation
- Simulate - Simulated navigation for testing
During navigation, the system provides:
- Turn-by-turn instructions with visual guidance
- Real-time trip progress (time, distance, ETA)
- GPS following and map updates
- Stop navigation capability
Navigation ends when:
- Destination is reached (automatic)
- User manually stops navigation
- Navigation error occurs
- Uses actual GPS position
- Updates based on real movement
- Responds to route deviations
- Provides real-time guidance
- Uses simulated movement along route
- Predictable progression for testing
- No GPS dependency
- Ideal for development and demos
The navigation system includes comprehensive error handling:
- No Route Available - Warning when no main route exists
- Navigation Start Failure - Error feedback when navigation cannot start
- GPS Issues - Graceful handling of position service problems
This navigation system provides a complete solution for both real-world navigation and development/testing scenarios. The modular architecture allows for easy customization and extension, while the integration with the Magic Lane SDK ensures reliable and accurate navigation functionality.
The separation between navigation and simulation modes enables developers to test navigation features without requiring physical movement, while providing users with professional-grade turn-by-turn navigation when needed.
