Skip to main content

External Position Source Navigation

Last updated: October 18, 2026 | 6 minutes read

This example demonstrates how to create a Flutter app that utilizes external position sources for navigation on a map using Maps SDK for Flutter. The app allows users to navigate to a predefined destination while following the route on the map.

How it works​

The example app demonstrates the following features:

  • Initialize a map.
  • Navigation using external position sources.
  • Allows route building and starts navigation with real-time position updates.
Initial map screen
Initial map screen
Computed route
Computed route
Navigating on route based on external positions
Navigating on route based on external positions

UI and Navigation Integration​

This code sets up the user interface, including a map and navigation buttons.

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'External Position Source Navigation',
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});


State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
late GemMapController _mapController;

late NavigationInstruction currentInstruction;
late DataSource _dataSource;

bool _areRoutesBuilt = false;
bool _isNavigationActive = false;

bool _hasDataSource = false;

// We use the handler to cancel the route calculation.
TaskHandler? _routingHandler;

// We use the handler to cancel the navigation.
TaskHandler? _navigationHandler;


void dispose() {
GemKit.release();
super.dispose();
}


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
"ExternalPositionNavigation",
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.deepPurple[900],
actions: [
if (!_isNavigationActive && _areRoutesBuilt)
IconButton(
onPressed: () => _startNavigation(),
icon: const Icon(Icons.play_arrow, color: Colors.white),
),
if (_isNavigationActive)
IconButton(
onPressed: _stopNavigation,
icon: const Icon(Icons.stop, color: Colors.white),
),
if (!_areRoutesBuilt && _hasDataSource)
IconButton(
onPressed: () => _onBuildRouteButtonPressed(context),
icon: const Icon(Icons.route, color: Colors.white),
),
if (!_isNavigationActive)
IconButton(
onPressed: _onFollowPositionButtonPressed,
icon: const Icon(
Icons.location_searching_sharp,
color: Colors.white,
),
),
],
),
body: Stack(
children: [
GemMap(
key: ValueKey("GemMap"),
onMapCreated: _onMapCreated,
appAuthorization: projectApiToken,
),
if (_isNavigationActive)
Positioned(
top: 10,
left: 10,
child: Column(
children: [
TopNavigationPanel(instruction: currentInstruction),
const SizedBox(height: 10),
FollowPositionButton(
onTap: () => _mapController.startFollowingPosition(),
),
],
),
),
if (_isNavigationActive)
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 10,
left: 0,
child: BottomNavigationPanel(
remainingDistance: getFormattedRemainingDistance(
currentInstruction,
),
remainingDuration: getFormattedRemainingDuration(
currentInstruction,
),
eta: getFormattedETA(currentInstruction),
),
),
],
),
resizeToAvoidBottomInset: false,
);
}

// The callback for when map is ready to use.
void _onMapCreated(GemMapController controller) {
// Save controller for further usage.
_mapController = controller;

_dataSource = DataSource.createExternalDataSource([DataType.position])!;
}

Handling Navigation and External Position Data​

This code handles building the route from a departure point to a destination, notifying the user when the calculation is in progress.

void _onBuildRouteButtonPressed(BuildContext context) {
// Define the departure
final departureLandmark = Landmark.withLatLng(
latitude: 34.915646,
longitude: -110.147933,
);

// Define the destination.
final destinationLandmark = Landmark.withLatLng(
latitude: 34.933105,
longitude: -110.131363,
);

// Define the route preferences.
final routePreferences = RoutePreferences();
_showSnackBar(context, message: 'The route is calculating.');

// Calling the calculateRoute SDK method.
// (err, results) - is a callback function that gets called when the route computing is finished.
// err is an error enum, results is a list of routes.
_routingHandler = RoutingService.calculateRoute(
[departureLandmark, destinationLandmark],
routePreferences,
(err, routes) {
// If the route calculation is finished, we don't have a progress listener anymore.
_routingHandler = null;

ScaffoldMessenger.of(context).clearSnackBars();

if (err == GemError.routeTooLong) {
print(
'The destination is too far from your current location. Change the coordinates of the destination.',
);
return;
}

// If there aren't any errors, we display the routes.
if (err == GemError.success) {
// Get the routes collection from map preferences.
final routesMap = _mapController.preferences.routes;

// Display the routes on map.
for (final route in routes) {
routesMap.add(
route,
route == routes.first,
label: getMapLabel(route),
);
}

// Center the camera on routes.
_mapController.centerOnRoutes(routes: routes);
setState(() {
_areRoutesBuilt = true;
});
}
},
);
}

Starting Navigation​

This method starts the navigation and sets the map to follow the user’s position.

Future<void> _startNavigation() async {
final routes = _mapController.preferences.routes;

if (routes.mainRoute == null) {
_showSnackBar(context, message: "Route is not available");
return;
}

_navigationHandler = NavigationService.startNavigation(
routes.mainRoute!,
onNavigationInstruction: (instruction, events) {
setState(() {
_isNavigationActive = true;
});
currentInstruction = instruction;
},
onError: (error) {
PositionService.removeDataSource();
_dataSource.stop();
setState(() {
_isNavigationActive = false;

_cancelRoute();
});
if (error != GemError.cancel) {
_stopNavigation();
}
return;
},
onDestinationReached: (landmark) {
PositionService.removeDataSource();
_dataSource.stop();
setState(() {
_isNavigationActive = false;

_cancelRoute();
});
_stopNavigation();
return;
},
);
// Set the camera to follow position.
_mapController.startFollowingPosition();

Pushing External Position Data​

This code manages the position data, updating the user’s location along the route at regular intervals.

Future<void> _pushExternalPosition() async {
final route = _mapController.preferences.routes.mainRoute;
final distance = route!.getTimeDistance().totalDistanceM;
Coordinates prevCoordinates = route.getCoordinateOnRoute(0);

// Parse route distance
for (
int currentDistance = 1;
currentDistance <= distance;
currentDistance += 1
) {
if (!_hasDataSource) return;

// Stop navigation if distance has been parsed
if (currentDistance == distance) {
_stopNavigation();
return;
}

// Get coordinate at current distance
final currentCoordinates = route.getCoordinateOnRoute(currentDistance);

await Future<void>.delayed(Duration(milliseconds: 25));

// Add each coordinate from route to data source immediately
_dataSource.pushData(
SenseDataFactory.producePosition(
acquisitionTime: DateTime.now(),
satelliteTime: DateTime.now(),
latitude: currentCoordinates.latitude,
longitude: currentCoordinates.longitude,
altitude: 0,
course: _getHeading(prevCoordinates, currentCoordinates),
speed: 0,
provider: Provider.gps,
fixQuality: PositionQuality.high,
),
);
prevCoordinates = currentCoordinates;
}
}

Top Navigation Instruction Panel​

top_navigation_panel.dartView on Github
class TopNavigationPanel extends StatelessWidget {
final NavigationInstruction instruction;

const TopNavigationPanel({super.key, required this.instruction});


Widget build(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width - 20,
height: MediaQuery.of(context).size.height * 0.2,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(20),
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(
getFormattedDistanceToNextTurn(instruction),
textAlign: TextAlign.left,
style: const TextStyle(
color: Colors.white,
fontSize: 25,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
Text(
instruction.nextStreetName,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}