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 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:

StyleController.addLayer(layer, {belowLayerId}) really does support inserting below an existing layer, matching the concept here. What the SDK does not expose is a way to list the loaded style's layers at runtime (there is no getStyleLayers()), so you can't auto-detect "the first symbol layer" the way the original tutorial did. In practice you already know your style — open its JSON (from the MapMetrics Portal, or fetch the initStyle URL) and note the id of the first "type": "symbol" layer once, then hardcode it as a constant.

Insert Layer Below Labels

Add a polygon fill that renders beneath all text labels:

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

class LayerBelowLabelsScreen extends StatefulWidget {
  @override
  _LayerBelowLabelsScreenState createState() =>
      _LayerBelowLabelsScreenState();
}

class _LayerBelowLabelsScreenState extends State<LayerBelowLabelsScreen> {
  MapController? mapController;

  // The id of the first label (symbol) layer in YOUR_STYLE.json.
  // Find it once by inspecting the style JSON — there is no runtime lookup.
  static const _firstSymbolLayerId = 'place-labels';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Layer Below Labels')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: Position(2.3522, 48.8566), // lng, lat
          initZoom: 12.0,
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
        ),
        onMapCreated: (MapController controller) {
          mapController = controller;
        },
        onStyleLoaded: (StyleController style) {
          _addLayerBelowLabels(style);
        },
      ),
    );
  }

  Future<void> _addLayerBelowLabels(StyleController style) async {
    // 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],
          ]
        ],
      },
    };

    await style.addSource(
      GeoJsonSource(id: 'highlight-area', data: jsonEncode(geoJson)),
    );

    // Insert fill layer BELOW the labels
    await style.addLayer(
      const FillStyleLayer(
        id: 'highlight-fill',
        sourceId: 'highlight-area',
        paint: {'fill-color': '#3b82f6', 'fill-opacity': 0.3},
      ),
      belowLayerId: _firstSymbolLayerId,
    );

    // Insert outline also below labels
    await style.addLayer(
      const LineStyleLayer(
        id: 'highlight-outline',
        sourceId: 'highlight-area',
        paint: {'line-color': '#1d4ed8', 'line-width': 2.0},
      ),
      belowLayerId: _firstSymbolLayerId,
    );
  }
}

Multiple Data Layers with Proper Ordering

Add several data layers that all sit below labels. Because there is no setLayerVisibility, toggles are implemented with removeLayer / addLayer(belowLayerId:):

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

class OrderedLayersScreen extends StatefulWidget {
  @override
  _OrderedLayersScreenState createState() => _OrderedLayersScreenState();
}

class _OrderedLayersScreenState extends State<OrderedLayersScreen> {
  MapController? mapController;
  StyleController? styleController;
  bool showZones = true;
  bool showRoutes = true;

  // Find this id once by inspecting YOUR_STYLE.json — no runtime lookup exists.
  static const _firstSymbolLayerId = 'place-labels';

  static const _zoneFillLayer = FillStyleLayer(
    id: 'zone-fill',
    sourceId: 'zones',
    paint: {'fill-color': '#22c55e', 'fill-opacity': 0.2},
  );
  static const _zoneOutlineLayer = LineStyleLayer(
    id: 'zone-outline',
    sourceId: 'zones',
    paint: {'line-color': '#15803d', 'line-width': 2.0},
  );
  static const _routeLineLayer = LineStyleLayer(
    id: 'route-line',
    sourceId: 'route',
    layout: {'line-join': 'round', 'line-cap': 'round'},
    paint: {'line-color': '#ef4444', 'line-width': 4.0},
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Ordered Layers')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(2.345, 48.857), // lng, lat
              initZoom: 12.0,
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            onStyleLoaded: (StyleController style) {
              styleController = style;
              _addOrderedLayers(style);
            },
          ),
          // 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!);
                            _toggleLayer(_zoneFillLayer, val!);
                            _toggleLayer(_zoneOutlineLayer, val!);
                          },
                        ),
                        Text('Zones'),
                      ],
                    ),
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Checkbox(
                          value: showRoutes,
                          onChanged: (val) {
                            setState(() => showRoutes = val!);
                            _toggleLayer(_routeLineLayer, val!);
                          },
                        ),
                        Text('Routes'),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _addOrderedLayers(StyleController style) async {
    // 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],
            ]],
          },
        },
      ],
    };

    await style.addSource(
      GeoJsonSource(id: 'zones', data: jsonEncode(zoneGeoJson)),
    );
    await style.addLayer(_zoneFillLayer, belowLayerId: _firstSymbolLayerId);
    await style.addLayer(_zoneOutlineLayer, belowLayerId: _firstSymbolLayerId);

    // 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],
        ],
      },
    };

    await style.addSource(
      GeoJsonSource(id: 'route', data: jsonEncode(routeGeoJson)),
    );
    await style.addLayer(_routeLineLayer, belowLayerId: _firstSymbolLayerId);
  }

  Future<void> _toggleLayer(StyleLayer layer, bool visible) async {
    final style = styleController;
    if (style == null) return;
    if (visible) {
      await style.addLayer(layer, belowLayerId: _firstSymbolLayerId);
    } else {
      await style.removeLayer(layer.id);
    }
  }
}

Layer Ordering

PositionLayer TypesLabels Visible?
Top (default)Data added without belowLayerIdCovered by data
Below labelsData added with belowLayerId: firstSymbolLayerIdYes, 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. Since this SDK has no runtime style introspection, open your style JSON once, note the id of its first symbol layer, and keep it as a constant in code.