Add a Layer Below Labels in Flutter
This tutorial shows how to insert new layers below the map's text labels — so your data layers don't cover up important place names and road labels.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Insert Layer Below Labels
Add a polygon fill that renders beneath all text labels:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class LayerBelowLabelsScreen extends StatefulWidget {
@override
_LayerBelowLabelsScreenState createState() =>
_LayerBelowLabelsScreenState();
}
class _LayerBelowLabelsScreenState extends State<LayerBelowLabelsScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Layer Below Labels')),
body: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8566, 2.3522),
zoom: 12.0,
),
onStyleLoaded: () {
_addLayerBelowLabels();
},
),
);
}
void _addLayerBelowLabels() {
// Find the first symbol (label) layer in the style
final firstSymbolLayer = _findFirstSymbolLayer();
// Add polygon source
final geoJson = {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.28, 48.84],
[2.42, 48.84],
[2.42, 48.88],
[2.28, 48.88],
[2.28, 48.84],
]
],
},
};
mapController?.addGeoJsonSource('highlight-area', geoJson);
// Insert fill layer BELOW the first label layer
mapController?.addFillLayer(
'highlight-fill',
'highlight-area',
fillColor: '#3b82f6',
fillOpacity: 0.3,
belowLayerId: firstSymbolLayer, // Insert below labels
);
// Insert outline also below labels
mapController?.addLineLayer(
'highlight-outline',
'highlight-area',
lineColor: '#1d4ed8',
lineWidth: 2.0,
belowLayerId: firstSymbolLayer,
);
}
/// Find the first symbol layer (text/label) in the map style
String? _findFirstSymbolLayer() {
final layers = mapController?.getStyleLayers();
if (layers != null) {
for (final layer in layers) {
if (layer.type == 'symbol') {
return layer.id;
}
}
}
return null; // If no symbol layer found, add on top
}
}Multiple Data Layers with Proper Ordering
Add several data layers that all sit below labels:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class OrderedLayersScreen extends StatefulWidget {
@override
_OrderedLayersScreenState createState() => _OrderedLayersScreenState();
}
class _OrderedLayersScreenState extends State<OrderedLayersScreen> {
MapMetricsController? mapController;
bool showZones = true;
bool showRoutes = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Ordered Layers')),
body: Stack(
children: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.857, 2.345),
zoom: 12.0,
),
onStyleLoaded: () {
_addOrderedLayers();
},
),
// Layer toggles
Positioned(
top: 16,
right: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: showZones,
onChanged: (val) {
setState(() => showZones = val!);
mapController?.setLayerVisibility(
'zone-fill', val!);
mapController?.setLayerVisibility(
'zone-outline', val!);
},
),
Text('Zones'),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: showRoutes,
onChanged: (val) {
setState(() => showRoutes = val!);
mapController?.setLayerVisibility(
'route-line', val!);
},
),
Text('Routes'),
],
),
],
),
),
),
),
],
),
);
}
void _addOrderedLayers() {
final firstSymbol = _findFirstSymbolLayer();
// Layer 1: Zone polygons (bottom)
final zoneGeoJson = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {'name': 'Zone A'},
'geometry': {
'type': 'Polygon',
'coordinates': [[
[2.30, 48.84], [2.36, 48.84],
[2.36, 48.87], [2.30, 48.87], [2.30, 48.84],
]],
},
},
{
'type': 'Feature',
'properties': {'name': 'Zone B'},
'geometry': {
'type': 'Polygon',
'coordinates': [[
[2.34, 48.85], [2.40, 48.85],
[2.40, 48.88], [2.34, 48.88], [2.34, 48.85],
]],
},
},
],
};
mapController?.addGeoJsonSource('zones', zoneGeoJson);
mapController?.addFillLayer(
'zone-fill', 'zones',
fillColor: '#22c55e',
fillOpacity: 0.2,
belowLayerId: firstSymbol,
);
mapController?.addLineLayer(
'zone-outline', 'zones',
lineColor: '#15803d',
lineWidth: 2.0,
belowLayerId: firstSymbol,
);
// Layer 2: Route lines (above zones, below labels)
final routeGeoJson = {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': [
[2.29, 48.86], [2.34, 48.855], [2.38, 48.86], [2.41, 48.865],
],
},
};
mapController?.addGeoJsonSource('route', routeGeoJson);
mapController?.addLineLayer(
'route-line', 'route',
lineColor: '#ef4444',
lineWidth: 4.0,
lineJoin: 'round',
lineCap: 'round',
belowLayerId: firstSymbol,
);
}
String? _findFirstSymbolLayer() {
final layers = mapController?.getStyleLayers();
if (layers != null) {
for (final layer in layers) {
if (layer.type == 'symbol') return layer.id;
}
}
return null;
}
}Layer Ordering
| Position | Layer Types | Labels Visible? |
|---|---|---|
| Top (default) | Data added without belowLayerId | Covered by data |
| Below labels | Data with belowLayerId: firstSymbol | Yes, readable |
| Bottom | Base map tiles | Always below everything |
Next Steps
- Add a GeoJSON Polygon — Draw polygons
- Change Layer Color — Dynamic layer styling
- Add a GeoJSON Line — Draw lines
Tip: Always insert data layers below labels in production apps. Users expect to see place names even when data overlays are active. Use getStyleLayers() to find the right insertion point.