
A robust, plug-and-play Flutter map engine for real-time courier/delivery tracking, with smooth animated markers, automatic rerouting, turn-by-turn voice navigation, and zero-cost routing via OSRM.
- Features
- Installation
- Quick Start
- DynamicMapWidget Parameters
- DynamicMapCubit — Full API Reference
- NavigationVoiceService — Full API Reference
- MarkerMotionService — Full API Reference
- LocationTrackingService — Full API Reference
- MapCameraService — Full API Reference
- RouteCalculationService — Full API Reference
- IconService — Full API Reference
- Cost Optimization
- Security Note
| Feature |
Description |
| 🗺️ Plug-and-Play Widget |
Drop DynamicMapWidget into any screen. No boilerplate. |
| 🎨 Gradient Polyline |
Route line rendered with smooth color gradient toward destination. |
| 🚗 Smooth Marker Animation |
Driver icon glides along the route — no jumping or stuttering. |
| 📍 Smart Road Snapping |
Driver marker stays clamped to the road polyline despite GPS jitter. |
| 🔄 Auto Rerouting |
Detects off-route movement and recalculates in background. |
| 🎙️ Voice Navigation |
Turn-by-turn TTS: "In 300 meters, turn right onto Jl. Sudirman" |
| 🔀 Roundabout Support |
Accurately announces the correct exit number: "Take the 2nd exit" |
| 🆓 Zero-Cost Routing |
Uses OSRM (OpenStreetMap) by default; falls back to Google Maps. |
| 🔐 Secure API Key |
API keys are never stored inside the package. You pass them in. |
| 📱 iOS & Android |
Optimized for both platforms, including background location. |
dependencies:
dynamic_maps_engine:
path: ../dynamic_maps_engine
dependencies:
dynamic_maps_engine: ^0.0.7
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:dynamic_maps_engine/dynamic_maps_engine.dart';
class DeliveryScreen extends StatelessWidget {
const DeliveryScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: DynamicMapWidget(
apiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
origin: const LatLng(-6.200000, 106.816666),
destination: const LatLng(-6.2100, 106.8200),
onLocationUpdated: (position) {
// Send to your backend every GPS tick
print('Driver: ${position.latitude}, ${position.longitude}');
},
),
);
}
}
| Parameter |
Type |
Required |
Default |
Description |
apiKey |
String |
✅ |
— |
Google Maps API key for map tiles & fallback routing |
origin |
LatLng |
✅ |
— |
Starting coordinate (driver's current location) |
destination |
LatLng |
✅ |
— |
Destination coordinate (customer address) |
onLocationUpdated |
Function(Position)? |
❌ |
null |
Callback fired on every GPS update |
driverIcon |
BitmapDescriptor? |
❌ |
Default pin |
Custom bitmap for driver marker |
destinationIcon |
BitmapDescriptor? |
❌ |
Default pin |
Custom bitmap for destination marker |
enableSimulation |
bool |
❌ |
false |
Simulate driving along the route (for testing) |
simulationSpeedKmh |
double |
❌ |
40.0 |
Speed of simulation in km/h |
The central state controller. Manages markers, polylines, camera, voice, and rerouting.
| Constant |
Type |
Value |
Description |
offRouteDistance |
double |
40.0 |
Meters off-route before rerouting is triggered |
offRouteThreshold |
int |
3 |
Consecutive off-route ticks before recalculating |
minSpeedForReroute |
double |
0.5 |
Min speed (m/s) for off-route detection to activate |
DynamicMapCubit({
required String apiKey,
BitmapDescriptor? driverIcon,
BitmapDescriptor? destinationIcon,
void Function(Position)? onLocationUpdated,
})
| Parameter |
Type |
Description |
apiKey |
String |
Google Maps API key |
driverIcon |
BitmapDescriptor? |
Custom driver marker icon |
destinationIcon |
BitmapDescriptor? |
Custom destination marker icon |
onLocationUpdated |
Function(Position)? |
Fired on every real GPS update (use for socket/backend sync) |
| Method |
Returns |
Description |
initialize({LatLng origin, LatLng destination, GoogleMapController? controller}) |
Future<void> |
One-time setup: loads route, sets up markers, starts GPS tracking |
selectRoute(int index) |
void |
Switch between alternative routes (index from state.availableRoutes) |
recalculateRoute() |
Future<void> |
Force reroute from current GPS position to existing destination |
toggleSimulation(bool enable, {double? speedKmh}) |
void |
Start/stop simulation mode. speedKmh defaults to last set value |
close() |
Future<void> |
Disposes all timers, streams, and voice service. Always call on dispose |
| Method |
Returns |
Description |
_setupMarkers(LatLng currentLocation, LatLng destination) |
Future<void> |
Creates driver and destination Marker objects with icons |
_calculateAndSetRoute(LatLng start, LatLng end) |
Future<void> |
Calls RouteCalculationService, builds polylines, loads voice steps |
_generateRoutePolylines(List<RouteData> routes, int selectedIndex) |
Set<Polyline> |
Builds all route polylines — selected route bold, alternatives faded |
_buildGradientPolylines(List<LatLng> points) |
Set<Polyline> |
Segments route into color-graded sections (green → red gradient) |
_startLocationTracking() |
void |
Starts LocationTrackingService stream and binds _handleLocationUpdate |
_handleLocationUpdate(Position position, {bool forceUpdate}) |
void |
Entry point for GPS updates: saves raw GPS, triggers marker animation and onLocationUpdated callback |
_updateDriverMarker(LatLng position, double bearing, int routeIndex) |
void |
Updates marker position & bearing; trims polyline to remaining route; recalculates live ETA |
_startRerouteCheck() |
void |
Starts a 2-second periodic timer that calls _checkOffRoute |
_checkOffRoute() |
void |
Measures driver distance from route polyline; increments _offRouteCount and calls recalculateRoute when threshold reached |
| Field |
Type |
Description |
markers |
Set<Marker> |
All visible map markers |
polylines |
Set<Polyline> |
Currently drawn polylines (remaining route) |
currentLocation |
LatLng? |
Latest known driver position |
destinationLocation |
LatLng? |
Target destination |
currentBearing |
double |
Current heading in degrees (0–360) |
currentSpeed |
double |
Smoothed speed in km/h (8-sample average) |
routeDistance |
String |
Remaining distance ("2.3 km") |
routeDuration |
String |
Remaining ETA ("7 min") |
routingEngine |
String |
"OSRM (Free)" or "Google Maps (Paid)" |
isRerouting |
bool |
True while recalculating route |
isSimulating |
bool |
True when simulation mode is active |
availableRoutes |
List<RouteData> |
All calculated route alternatives |
selectedRouteIndex |
int |
Index of active route |
Handles all turn-by-turn navigation logic: step detection, voice announcements, distance calculation.
| Getter |
Type |
Description |
isNavigating |
bool |
Whether navigation is currently active |
currentInstruction |
String? |
Instruction for the current active step (e.g. "Turn right") |
nextInstruction |
String? |
Instruction for the next upcoming step |
currentManeuverIcon |
String? |
Icon name for current maneuver (e.g. "turn_right") |
nextManeuverIcon |
String? |
Icon name for upcoming maneuver |
currentSpeed |
double |
Last known speed in km/h |
nextStepDistance |
double |
Distance to end of current step in meters |
remainingDistance |
double |
Total remaining distance (meters) to destination |
remainingDuration |
double |
Estimated remaining duration in seconds |
formattedRemainingDistance |
String |
Human readable: "1.2 km" or "850 m" |
formattedRemainingDuration |
String |
Human readable: "5 min" or "1 hr 12 min" |
currentSteps |
List<NavigationStep> |
All loaded navigation steps |
currentStep |
NavigationStep? |
Currently active step object |
roundaboutExit |
int? |
Exit number if current step is a roundabout |
| Method |
Returns |
Description |
startNavigation(List<Map<String, dynamic>> stepsData) |
Future<void> |
Loads step data, initializes TTS engine, sets first instruction |
updatePosition(Position position) |
void |
Main update loop. Feed GPS positions here; handles step advancement and voice triggers |
getManeuverIcon(String instruction, {String? maneuver}) |
String |
Returns icon name string for a given maneuver type or instruction text |
dispose() |
void |
Stops TTS, cancels timers, clears all state |
| Method |
Returns |
Description |
_initializeTts() |
Future<void> |
Sets up flutter_tts engine: language (id-ID), pitch, rate, volume. Must be awaited before any speech. |
_updateDisplayInstructions() |
void |
Refreshes _currentInstruction, _nextInstruction, _currentManeuverIcon, _nextManeuverIcon from current step index. Called after every step change. |
_moveToNextStep() |
void |
Advances _currentStepIndex by 1, resets warning flags, calls _updateDisplayInstructions() |
_snapToClosestStep(Position position) |
void |
If driver position is closest to a later step (not current), jumps _currentStepIndex forward. Prevents stuck navigation on wrong step. |
_checkNavigationProgress(Position position) |
void |
Core logic: checks distance to step end, fires 300m/20m voice warnings, advances to next step when arrived |
_calculateDistanceAlongPolyline(LatLng position, List<LatLng> polyline) |
double |
Calculates remaining meters along a step's polyline (not straight-line), for accurate ETA |
_roundDistanceForSpeech(double meters) |
int |
Rounds distance to nearest 50m or 100m for clean voice output ("In 300 meters", not "In 287 meters") |
_cleanHtmlInstruction(String html) |
String |
Strips HTML tags from Google Maps instructions (<b>, <div>, etc.) |
| Field |
Type |
Description |
instruction |
String |
Cleaned human-readable instruction |
distance |
double |
Step distance in meters |
duration |
double |
Step duration in seconds |
startLat / startLng |
double |
Coordinate of step start |
endLat / endLng |
double |
Coordinate of step end |
maneuver |
String? |
Raw maneuver type ("turn-right", "roundabout", etc.) |
roundaboutExit |
int? |
Exit number for roundabouts (1 = first exit) |
polylinePoints |
List<LatLng> |
All coordinates along this step's path |
isRoundabout |
bool |
Shortcut: maneuver?.contains('roundabout') == true |
formattedDistance |
String |
Human readable distance ("300m", "1.2km") |
formattedDuration |
String |
Human readable duration ("2 min") |
Smoothly animates a map marker from one position to another using frame-by-frame interpolation.
| Getter |
Type |
Description |
bearing |
double |
Current heading angle in degrees (0–360) |
currentPosition |
LatLng? |
Current interpolated marker position |
currentRouteIndex |
int |
Current index in the route polyline |
| Method |
Returns |
Description |
setRoutePoints(List<LatLng> points) |
void |
Sets the full route polyline for snapping and simulation |
reset() |
void |
Resets all position, bearing, and index state |
injectPosition(LatLng position, double bearing) |
void |
Force-sets position without animation (used when teleporting) |
moveTo({required LatLng target, required Function(LatLng, double, int) onUpdate, double? initialBearing, double? heading}) |
void |
Animate marker to target. onUpdate called each frame with (position, bearing, routeIndex) |
startSimulation(Function(LatLng, double, int) onUpdate, {double speedKmh, bool fromBeginning}) |
void |
Begins automatic route simulation at given speed. Calls onUpdate at ~25fps |
stopSimulation() |
void |
Stops simulation timer |
dispose() |
void |
Cancels all timers and cleans up resources |
| Method |
Returns |
Description |
_moveAlongRoute(LatLng target, Function onUpdate) |
void |
When route points exist, snaps to nearest route point and animates along road polyline |
_smoothMove(LatLng target, Function onUpdate) |
void |
Fallback animation when no route points set — interpolates in straight line |
_findNearestRoutePoint(LatLng position) |
int |
Returns index of closest route point to given position |
_moveTowards(LatLng current, LatLng target, double distanceMeters) |
LatLng |
Moves distanceMeters from current toward target along great circle |
_destinationPoint(LatLng start, double distance, double bearing) |
LatLng |
Calculates destination given start point, distance (m), and bearing |
_lerp(LatLng a, LatLng b, double t) |
LatLng |
Linear interpolation between two coordinates (t = 0.0 to 1.0) |
_calculateBearing(LatLng start, LatLng end) |
double |
Calculates compass bearing between two GPS points in degrees |
_blendBearing(double current, double target, double factor) |
double |
Smoothly blends bearing to avoid 359°→1° flipping artifacts |
_distanceInMeters(LatLng a, LatLng b) |
double |
Haversine distance between two points in meters |
Wraps Geolocator to provide filtered, platform-optimized GPS streams.
| Getter |
Type |
Description |
isTracking |
bool |
Whether tracking is currently active |
| Method |
Returns |
Description |
getCurrentPosition({LocationAccuracy accuracy}) |
Future<Position?> |
One-shot: gets current GPS fix. Returns null on failure. |
getLastKnownPosition() |
Future<Position?> |
Returns last cached GPS position (fast, may be stale) |
startTracking({required Function(Position) onPositionUpdate, Duration? interval, bool useStream}) |
void |
Start continuous GPS tracking. Always uses stream mode (recommended). |
pauseTracking() |
void |
Temporarily stops the position stream without clearing state |
resumeTracking() |
void |
Resumes a paused stream |
stopTracking() |
void |
Fully stops tracking and clears all listeners |
updateLocationSettings({int? interval, double? distanceFilter}) |
Future<void> |
Hot-update GPS interval and distance filter without restarting |
dispose() |
void |
Stops tracking and releases all resources |
| Method |
Returns |
Description |
_startStreamTracking(Function(Position) onPositionUpdate) |
void |
Subscribes to Geolocator.getPositionStream() with distanceFilter: 0. All positions filtered by _shouldUpdate() |
_startTimerTracking(Function(Position) onPositionUpdate, Duration interval) |
void |
Alternative: polls getCurrentPosition() on a timer. Used only if useStream: false |
_shouldUpdate(Position newPosition) |
bool |
Returns true if driver moved >2m (iOS) / >3m (Android) OR >8s (iOS) / >10s (Android) since last update |
| Setting |
iOS |
Android |
| Distance threshold |
2.0 m |
3.0 m |
| Time threshold |
8 seconds |
10 seconds |
| GPS update interval |
~1s (system) |
~1s (system) |
Controls Google Map camera behavior: following driver, fitting route, manual mode.
| Mode |
Enum |
Description |
| Following |
CameraMode.following |
Camera locks to driver, rotates with bearing |
| Center |
CameraMode.center |
Camera centers on driver, no rotation |
| AutoFit |
CameraMode.autoFit |
Camera zooms out to show full route |
| Manual |
CameraMode.manual |
User is panning — camera unlocked |
| Property |
Type |
Description |
currentZoom |
double |
Current map zoom level (default: 17.5) |
isProgrammaticMove |
bool |
True while the engine is animating the camera (ignore user-pan events) |
isFollowing |
bool |
True when mode is CameraMode.following |
isCenter |
bool |
True when mode is CameraMode.center |
isAutoFit |
bool |
True when mode is CameraMode.autoFit |
| Method |
Returns |
Description |
setController(GoogleMapController controller) |
void |
Binds the GoogleMapController after map is created |
setMode(CameraMode mode) |
void |
Switch camera mode |
activateManualMode({Duration? autoResetDuration}) |
void |
Lock camera to manual (user panning). Auto-resets to following after autoResetDuration (default: 10s) |
followUser(Position position, {bool isSimulating}) |
Future<void> |
Animate camera to follow driver with tilt and bearing. Uses animateCamera. |
centerToLocation(LatLng location, {double zoom}) |
Future<void> |
Move camera to a fixed location at given zoom |
fitToRoute(LatLng start, LatLng destination, {bool isSimulating}) |
Future<void> |
Fit camera to show entire route between two points |
fitBoundsWithPadding(LatLng start, LatLng end, {bool isRerouting}) |
Future<void> |
Fit camera to bounds with adaptive padding |
droneArrival(Position position) |
Future<void> |
Cinematic arrival animation: zoom in, tilt down, rotate to face destination |
| Method |
Returns |
Description |
_smoothFitBounds(LatLng start, LatLng end, double padding, {bool isSimulating}) |
Future<void> |
Calculates LatLngBounds and calls animateCamera with padding |
_getOptimalPadding(double distanceInMeters, {bool isRerouting}) |
double |
Returns padding value (30–120) scaled to route length |
Calculates routes using OSRM (free) with automatic Google Maps Directions API fallback.
RouteCalculationService({required String apiKey})
| Method |
Returns |
Description |
calculateRoutes(LatLng origin, LatLng destination) |
Future<List<RouteData>> |
Calculates all available routes (with alternatives). Tries OSRM → Google Maps → Fallback. |
calculateRoute(LatLng origin, LatLng destination) |
Future<RouteData> |
Convenience: returns first (best) route from calculateRoutes |
calculateDistance(LatLng point1, LatLng point2) |
double |
Haversine distance in meters between two points |
| Method |
Returns |
Description |
_parseDirectionsResponseMulti(Map data) |
List<RouteData> |
Parses full Google Maps Directions API JSON response into list of RouteData |
_getAlternativeRoute(LatLng origin, LatLng destination) |
Future<RouteData?> |
Calls OSRM route/v1/driving/ API and parses geometry + steps into RouteData |
_getFallbackRoute(LatLng origin, LatLng destination) |
RouteData |
Offline fallback: generates a grid-based straight-line route estimate |
_generateGridBasedRoute(LatLng origin, LatLng destination) |
List<LatLng> |
Generates a 2-segment curved approximation of the path |
_addIntermediatePoints(List<LatLng> points, LatLng start, LatLng end, int count) |
void |
Inserts count evenly-spaced points between start and end |
_decodePolyline(String encoded) |
List<LatLng> |
Decodes Google Maps encoded polyline string into coordinate list |
_estimateDistance(LatLng start, LatLng end) |
double |
Estimates driving distance (km) from straight-line × 1.25 factor |
_estimateDuration(LatLng start, LatLng end) |
int |
Estimates duration (minutes) assuming average 30 km/h |
calculateRoutes()
│
▼
Try Google Maps (paid) ──(OK)──▶ _parseDirectionsResponseMulti() ──▶ return routes
│
(fail / empty API key)
│
▼
Try OSRM (free) ──────(OK)──▶ parse geometry + steps ──▶ return route
│
(fail)
│
▼
_getFallbackRoute() ────────────▶ grid estimate ──▶ return offline route
| Field |
Type |
Description |
points |
List<LatLng> |
Full polyline coordinates |
duration |
String |
Human readable ETA ("7 min") |
distance |
String |
Human readable distance ("2.3 km") |
steps |
List<Map<String, dynamic>> |
Turn-by-turn instruction steps |
routingEngine |
String |
"OSRM (Free)" or "Google Maps (Paid)" |
| Key |
Type |
Description |
instruction |
String |
Cleaned instruction text |
distance |
num |
Step distance in meters |
duration |
num |
Step duration in seconds |
polyline |
String |
Encoded polyline string (Google) or empty (OSRM) |
start_lat / start_lng |
double |
Step start coordinate |
end_lat / end_lng |
double |
Step end coordinate |
maneuver |
String? |
Maneuver type ("turn-right", "roundabout", etc.) |
roundaboutExit |
int? |
Exit number for roundabout steps (OSRM only) |
Pre-loads and caches custom bitmap icons for use as Google Maps markers.
| Method |
Returns |
Description |
preloadDriverIcon({Color color}) |
Future<void> |
Pre-renders driver chevron icon in given color. Default: Colors.blue |
getDriverIcon() |
BitmapDescriptor |
Returns pre-loaded driver icon. Falls back to blue default pin if not preloaded. |
getUserLocationIcon() |
BitmapDescriptor |
Returns user location dot icon |
getDestinationIcon() |
BitmapDescriptor |
Returns destination icon. Falls back to green default pin. |
getRealGpsIcon() |
BitmapDescriptor |
Returns raw GPS position indicator (debug use) |
getConnectionDotIcon() |
BitmapDescriptor |
Returns animated connection status dot |
getTurnArrowIcon() |
BitmapDescriptor |
Returns turn arrow icon for navigation overlay |
| Method |
Returns |
Description |
_createDriverChevron({Color color, double size}) |
Future<BitmapDescriptor> |
Renders a canvas-drawn chevron shape as a bitmap at given size pixels |
_createUserLocationIcon({Color color}) |
Future<BitmapDescriptor> |
Renders a pulsing dot icon for user location indicator |
_createConnectionDot() |
Future<BitmapDescriptor> |
Renders a small dot icon for connection status overlay |
_createTurnArrow() |
Future<BitmapDescriptor> |
Renders a turn arrow for navigation UI overlay |
// In your init (before opening map screen)
await IconService.preloadDriverIcon(color: Colors.blue);
// Pass to DynamicMapWidget or DynamicMapCubit
DynamicMapCubit(
apiKey: key,
driverIcon: IconService.getDriverIcon(),
destinationIcon: IconService.getDestinationIcon(),
)
By default, routing uses OSRM (100% free, OpenStreetMap). Only falls back to Google Maps Directions API (paid) when OSRM is unavailable.
| Service |
Google Maps API Cost |
OSRM (This Package) Cost |
Savings |
| Routing / Directions |
~$5.00 / 1000 requests |
$0.00 |
100% |
| Turn-by-turn Steps |
Included in Directions |
$0.00 |
100% |
| Map Tiles Rendering |
~$7.00 / 1000 loads |
Normal GMap Cost |
— |
| Geocoding |
~$5.00 / 1000 requests |
Use OSM Nominatim |
~95%+ |
This package never reads .env files internally. Always pass your apiKey from your application's own secure environment setup.
// ✅ Correct
DynamicMapWidget(apiKey: dotenv.env['GOOGLE_MAPS_KEY']!, ...)
// ❌ Never hardcode
DynamicMapWidget(apiKey: 'AIzaSy...', ...)
MIT License © 2024 Rayhan Rwa