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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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:
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:
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:
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
| Property | Type | Description |
|---|---|---|
polygons | List<Polygon> | Geometries to draw, Polygon(coordinates: [[Position(lng, lat), ...]]) |
color | Color | Fill color (default: black — use Color.withValues(alpha: ...) for transparency) |
outlineColor | Color | Border color (default: black) |
Next Steps
- Add a Polyline — Draw lines and routes
- Add a Cluster — Group nearby markers
- Add a Popup — Show popups on tap
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.