Skip to content

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:

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

PositionLayer TypesLabels Visible?
Top (default)Data added without belowLayerIdCovered by data
Below labelsData with belowLayerId: firstSymbolYes, readable
BottomBase map tilesAlways below everything

Next Steps


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.