# Draw Roadblock

Last updated:

<!-- -->

October 18, 2026

<!-- -->

\| 4 minutes read

<br />

<br />

This example showcases how to build a Flutter app featuring an interactive map. Users can draw paths on map, whose coordinates can be used to confirm a roadblock.

## How it works[​](#how-it-works "Direct link to How it works")

The example app includes the following features:

* Display a map.
* Draw a roadblock in one or multiple steps.
* Display preview paths on map.
* Confirm a persistent Roadblock based on the drawn path.

![Initial map screen](/docs/flutter/assets/images/example_flutter_draw_road_block1-db63e3d62b4c8890e8a0458e704dfed6.png)

**Initial map screen**

![Started drawing preview path](/docs/flutter/assets/images/example_flutter_draw_road_block2-e9fed8b3f6ada7746b438a85a85532dc.png)

**Started drawing preview path**

<br />

![Confirmed previous path (in red)](/docs/flutter/assets/images/example_flutter_draw_road_block3-f377a8dae64297be99182f4fa1fdc1a9.png)

**Confirmed previous path (in red)**

![Confirmed roadblock on map](/docs/flutter/assets/images/example_flutter_draw_road_block4-b68ab5d23ff40eb0d4524e1a1183f7e4.png)

**Confirmed roadblock on map**

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

The code below builds a user interface featuring an interactive GemMap and an app bar with action buttons to start drawing mode, confirm the drawn path, cancel drawing, and confirm a roadblock.

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

```dart
const projectApiToken = String.fromEnvironment('YOUR_API_TOKEN_HERE');

void main() async {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Draw Roadblock',
      home: MyHomePage(),
    );
  }
}

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

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

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

  // The coordinate where the preview currently ends (acts as a cursor for preview path)
  UserRoadblockPathPreviewCoordinate? previewCursor;

  // List of permanent coordinates (user confirmed)
  List<Coordinates> permanentCoords = [];
  // List of preview (temporary) coordinates
  List<Coordinates> previewCoordsList = [];

  // Draw mode
  bool drawMode = false;

  /// Debouncer in order to calculate preview only after movement is stopped
  Timer? _debounce;

  @override
  void initState() {
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: true,
        foregroundColor: Colors.white,
        title: const Text(
          "Draw Roadblock",
          style: TextStyle(color: Colors.white),
        ),
        backgroundColor: Colors.deepPurple[900],
        actions: [
          // Add temporary coordinates to permanent
          if (drawMode)
            IconButton(
              icon: Icon(Icons.add),
              onPressed: () {
                permanentCoords = [...permanentCoords, ...previewCoordsList];
                previewCoordsList = [];
                _redrawPath();
              },
            ),
          // Draw mode activate button
          if (!drawMode)
            IconButton(
              icon: Icon(Icons.draw),
              onPressed: () {
                setState(() {
                  drawMode = true;
                  permanentCoords = [];
                  previewCursor = null;
                  previewCoordsList = [];
                });
                _handlePreviewPathUpdate(allowRecursive: false);
              },
            ),
          // Add roadblock by permanent coordinates and reset the draw mode and coordinates lists
          if (drawMode)
            IconButton(
              icon: Icon(Icons.check),
              onPressed: () {
                final roadblockResult =
                    TrafficService.addPersistentRoadblockByCoordinates(
                  coords: permanentCoords,
                  startTime: DateTime.now(),
                  expireTime: DateTime.now().add(const Duration(days: 1)),
                  transportMode: RouteTransportMode.car,
                  id: DateTime.now().toIso8601String(), // Unique identifier
                );

                setState(() {
                  drawMode = false;
                  permanentCoords = [];
                  previewCursor = null;
                  previewCoordsList = [];
                });

                // In case of error, show snackbar
                if (roadblockResult.$2 != GemError.success) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text(
                        'Error ${roadblockResult.$2} when adding roadblock.',
                      ),
                      backgroundColor: Colors.red,
                      duration: Duration(seconds: 3),
                    ),
                  );
                }

                // Reset the coordinates
                permanentCoords = [];
                previewCoordsList = [];
                _redrawPath();
              },
            ),
          // Cancel draw mode
          if (drawMode)
            IconButton(
              icon: Icon(Icons.cancel),
              onPressed: () {
                setState(() {
                  drawMode = false;
                  permanentCoords = [];
                  previewCoordsList = [];
                });
                permanentCoords = [];
                previewCoordsList = [];
                previewCursor = null;
                _redrawPath();
              },
            ),
        ],
      ),
      body: Stack(
        children: [
          GemMap(
            appAuthorization: projectApiToken,
            onMapCreated: _onMapCreated,
          ),
          // Mark the center of the viewport
          const Center(child: Icon(Icons.add, size: 32, color: Colors.black)),
        ],
      ),
    );
  }

  /// Called when the map is created. Registers the move callback for draw mode.
  void _onMapCreated(GemMapController controller) async {
    _mapController = controller;

    _mapController.registerOnMove((_, __) {
      if (!drawMode) return;

      _debounce?.cancel();
      _debounce = Timer(const Duration(milliseconds: 100), () {
        _handlePreviewPathUpdate(allowRecursive: true);
      });
    });
  }

  /// Handles updating the preview path when the map moves.
  void _handlePreviewPathUpdate({required bool allowRecursive}) {
    final centerScreen = Point<int>(
      _mapController.viewport.width ~/ 2,
      _mapController.viewport.height ~/ 2,
    );
    final centerCoord = _mapController.transformScreenToWgs(centerScreen);

    // On first call, initialize permanentCoords with the center coordinate
    if (permanentCoords.isEmpty) {
      permanentCoords.add(centerCoord);
    }

    // If previewCursor is null, set it and return
    if (previewCursor == null) {
      previewCursor = UserRoadblockPathPreviewCoordinate.fromCoordinates(
        centerCoord,
      );
      return;
    }

    // Compute route coordinates from previous coordinates to new coordinates
    final (from, to, error) = TrafficService.getPersistentRoadblockPathPreview(
      from: previewCursor!,
      to: centerCoord,
      transportMode: RouteTransportMode.car,
    );

    if (error != GemError.success) {
      _resetPreviewPathOnError(allowRecursive);
      return;
    }

    _updatePreviewPath(from, to);
  }

  /// Resets the preview path and optionally retries if an error occurs.
  void _resetPreviewPathOnError(bool allowRecursive) {
    previewCoordsList = [];
    previewCursor = UserRoadblockPathPreviewCoordinate.fromCoordinates(
      permanentCoords.last,
    );
    _redrawPath();
    if (allowRecursive) {
      _handlePreviewPathUpdate(allowRecursive: false);
    }
  }

  /// Updates the preview path and redraws the map.
  void _updatePreviewPath(
    List<Coordinates> from,
    UserRoadblockPathPreviewCoordinate to,
  ) {
    previewCoordsList = [...previewCoordsList, ...from];
    previewCursor = to;
    _redrawPath();
  }

  /// Redraws the permanent and preview paths on the map.
  void _redrawPath() {
    final previewPath = Path.fromCoordinates(previewCoordsList);
    final permanentPath = Path.fromCoordinates(permanentCoords);
    _mapController.preferences.paths.clear();
    _mapController.preferences.paths.add(previewPath, colorInner: Colors.green);
    _mapController.preferences.paths.add(permanentPath, colorInner: Colors.red);
  }
}

```
