Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Add a Polygon in Flutter ​

This tutorial shows how to draw filled polygons on your MapMetrics Flutter map. Polygons are useful for highlighting areas, zones, or boundaries.

Prerequisites ​

Before you begin, ensure you have:

Basic Polygon ​

Draw a polygon with a PolygonLayer. Its geometry comes from a Polygon, whose coordinates is a list of linear rings, each a list of Position(lng, lat) points — longitude first. The ring is automatically closed by MapLibre when the first and last point match:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

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

  @override
  State<PolygonExampleScreen> createState() => _PolygonExampleScreenState();
}

class _PolygonExampleScreenState extends State<PolygonExampleScreen> {
  MapController? mapController;

  final _manhattan = [
    Polygon(
      coordinates: [
        [
          Position(-73.958, 40.800),
          Position(-74.020, 40.800),
          Position(-74.020, 40.700),
          Position(-73.970, 40.700),
          Position(-73.958, 40.800),
        ],
      ],
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Polygon Example')),
      body: MapMetricsView(
        options: MapOptions(
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
          initCenter: Position(-73.990, 40.750),
          initZoom: 12,
        ),
        onMapCreated: (controller) => mapController = controller,
        layers: [
          PolygonLayer(
            polygons: _manhattan,
            color: Colors.blue.withValues(alpha: 0.2),
            outlineColor: Colors.blue,
          ),
        ],
      ),
    );
  }
}

Multiple Colored Zones ​

A single PolygonLayer applies one color/outlineColor to every polygon it holds, so to show zones in different colors, add one PolygonLayer per color:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

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

  @override
  State<ColoredZonesScreen> createState() => _ColoredZonesScreenState();
}

class _ColoredZonesScreenState extends State<ColoredZonesScreen> {
  MapController? mapController;

  final _zoneA = [
    Polygon(
      coordinates: [
        [
          Position(2.330, 48.870),
          Position(2.350, 48.870),
          Position(2.350, 48.860),
          Position(2.330, 48.860),
          Position(2.330, 48.870),
        ],
      ],
    ),
  ];

  final _zoneB = [
    Polygon(
      coordinates: [
        [
          Position(2.330, 48.860),
          Position(2.350, 48.860),
          Position(2.350, 48.850),
          Position(2.330, 48.850),
          Position(2.330, 48.860),
        ],
      ],
    ),
  ];

  final _zoneC = [
    Polygon(
      coordinates: [
        [
          Position(2.330, 48.850),
          Position(2.350, 48.850),
          Position(2.350, 48.840),
          Position(2.330, 48.840),
          Position(2.330, 48.850),
        ],
      ],
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Colored Zones')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              initCenter: Position(2.340, 48.855),
              initZoom: 14,
            ),
            onMapCreated: (controller) => mapController = controller,
            layers: [
              // Zone A - Green (safe zone)
              PolygonLayer(
                polygons: _zoneA,
                color: Colors.green.withValues(alpha: 0.25),
                outlineColor: Colors.green,
              ),
              // Zone B - Orange (caution zone)
              PolygonLayer(
                polygons: _zoneB,
                color: Colors.orange.withValues(alpha: 0.25),
                outlineColor: Colors.orange,
              ),
              // Zone C - Red (restricted zone)
              PolygonLayer(
                polygons: _zoneC,
                color: Colors.red.withValues(alpha: 0.25),
                outlineColor: Colors.red,
              ),
            ],
          ),
          // Legend
          Positioned(
            top: 16,
            right: 16,
            child: Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.white.withValues(alpha: 0.9),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _legendItem(Colors.green, 'Safe Zone'),
                  const SizedBox(height: 6),
                  _legendItem(Colors.orange, 'Caution Zone'),
                  const SizedBox(height: 6),
                  _legendItem(Colors.red, 'Restricted Zone'),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _legendItem(Color color, String label) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: 16,
          height: 16,
          decoration: BoxDecoration(
            color: color.withValues(alpha: 0.3),
            border: Border.all(color: color, width: 2),
            borderRadius: BorderRadius.circular(3),
          ),
        ),
        const SizedBox(width: 8),
        Text(label, style: const TextStyle(fontSize: 13)),
      ],
    );
  }
}

Tappable Polygons ​

PolygonLayer doesn't expose a per-polygon onTap callback the way some other SDKs do. Instead, listen for MapEventClick on MapMetricsView.onEvent — it gives you the tapped Position — and add the layer through StyleController with a known id so you can look up which layer was hit with MapController.queryLayers:

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

  @override
  State<TappableAreaScreen> createState() => _TappableAreaScreenState();
}

class _TappableAreaScreenState extends State<TappableAreaScreen> {
  MapController? mapController;

  static const _sourceId = 'tappable-area';
  static const _layerId = 'tappable-area-fill';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Tappable Polygon')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: Position(2.345, 48.860),
          initZoom: 13,
        ),
        onMapCreated: (controller) => mapController = controller,
        onStyleLoaded: (style) async {
          await style.addSource(
            const GeoJsonSource(
              id: _sourceId,
              data: '''
                {
                  "type": "Feature",
                  "geometry": {
                    "type": "Polygon",
                    "coordinates": [[
                      [2.330, 48.870], [2.360, 48.870],
                      [2.360, 48.850], [2.330, 48.850], [2.330, 48.870]
                    ]]
                  },
                  "properties": {}
                }
              ''',
            ),
          );
          await style.addLayer(
            const FillStyleLayer(
              id: _layerId,
              sourceId: _sourceId,
              paint: {
                'fill-color': '#9c27b0',
                'fill-opacity': 0.2,
              },
            ),
          );
        },
        onEvent: (event) async {
          if (event case MapEventClick(:final point)) {
            final screenPoint = await mapController!.toScreenLocation(point);
            final features = await mapController!.queryLayers(screenPoint);
            final hit = features.any((f) => f['layerId'] == _layerId);
            if (hit) {
              showDialog(
                context: context,
                builder: (context) => AlertDialog(
                  title: const Text('Area Selected'),
                  content: const Text('You tapped on the highlighted area.'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('OK'),
                    ),
                  ],
                ),
              );
            }
          }
        },
      ),
    );
  }
}

Polygon Properties ​

PropertyTypeDescription
polygonsList<Polygon>Geometries to draw, Polygon(coordinates: [[Position(lng, lat), ...]])
colorColorFill color (default: black — use Color.withValues(alpha: ...) for transparency)
outlineColorColorBorder color (default: black)

Next Steps ​


Tip: Use Color.withValues(alpha: ...) on your fill colors to keep the polygon semi-transparent so the map underneath remains visible. PolygonLayer styles every polygon in its polygons list identically — stack several PolygonLayers if you need per-area colors.