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.