# External Position Source Navigation

Last updated:

<!-- -->

October 18, 2026

<!-- -->

\| 6 minutes read

<br />

<br />

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[​](#how-it-works "Direct link to 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](/docs/flutter/assets/images/example_flutter_external_position_source_navigation1-f31188bb837740df2cacd9cd07b82388.png)

**Initial map screen**

![Computed route](/docs/flutter/assets/images/example_flutter_external_position_source_navigation2-648b185ea13dbb5982343ca7bfcc9e7b.png)

**Computed route**

![Navigating on route based on external positions](/docs/flutter/assets/images/example_flutter_external_position_source_navigation3-a91feaa5714a534270384d0a5a9e698a.png)

**Navigating on route based on external positions**

### UI and Navigation Integration[​](#ui-and-navigation-integration "Direct link to UI and Navigation Integration")

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

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/external_position_source_navigation/lib/main.dart?ref_type=heads#L24)

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

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

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

  @override
  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;

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

  @override
  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[​](#handling-navigation-and-external-position-data "Direct link to 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.

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/external_position_source_navigation/lib/main.dart?ref_type=heads#L151)

```dart
  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[​](#starting-navigation "Direct link to Starting Navigation")

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

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/external_position_source_navigation/lib/main.dart?ref_type=heads#L211)

```dart
  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[​](#pushing-external-position-data "Direct link to Pushing External Position Data")

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

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/external_position_source_navigation/lib/main.dart?ref_type=heads#L259)

```dart
  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-instruction-panel "Direct link to Top Navigation Instruction Panel")

top\_navigation\_panel.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/external_position_source_navigation/lib/top_navigation_panel.dart?ref_type=heads#L13)

```dart
class TopNavigationPanel extends StatelessWidget {
  final NavigationInstruction instruction;

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

  @override
  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,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

```
