Skip to main content

External Position Source Navigation

Last updated: July 15, 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
Computed route
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,
),
],
),
),
],
),
);
}
}