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

Change a Layer's Color with Buttons in Flutter

This tutorial shows how to dynamically change the color of a map layer at runtime using buttons — no need to reload the map.

Prerequisites

Before you begin, ensure you have:

There is no mapController.addGeoJsonSource(...), addFillLayer(...), addLineLayer(...), or setPaintProperty(...). Sources and layers go through StyleController.addSource(Source) / StyleController.addLayer(StyleLayer), and because StyleController has no "update a paint property" call, changing color at runtime means removeLayer followed by addLayer with the new paint — which is cheap here since these are layers you define yourself and can always fully re-declare.

Change Fill Layer Color

Add a polygon and change its color by tapping buttons:

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

class ChangeLayerColorScreen extends StatefulWidget {
  @override
  _ChangeLayerColorScreenState createState() =>
      _ChangeLayerColorScreenState();
}

const _sourceId = 'region';
const _fillLayerId = 'region-fill';
const _outlineLayerId = 'region-outline';

class _ChangeLayerColorScreenState extends State<ChangeLayerColorScreen> {
  MapController? mapController;
  StyleController? styleController;
  String currentColor = '#3b82f6';

  final List<Map<String, dynamic>> colorOptions = [
    {'name': 'Blue', 'hex': '#3b82f6', 'color': Colors.blue},
    {'name': 'Red', 'hex': '#ef4444', 'color': Colors.red},
    {'name': 'Green', 'hex': '#22c55e', 'color': Colors.green},
    {'name': 'Purple', 'hex': '#8b5cf6', 'color': Colors.purple},
    {'name': 'Orange', 'hex': '#f59e0b', 'color': Colors.orange},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Change Layer Color')),
      body: Column(
        children: [
          // Color buttons
          Container(
            padding: EdgeInsets.all(12),
            child: Wrap(
              spacing: 8,
              children: colorOptions.map((option) {
                return ElevatedButton(
                  onPressed: () => _changeColor(option['hex']),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: option['color'],
                    foregroundColor: Colors.white,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                  ),
                  child: Text(option['name']),
                );
              }).toList(),
            ),
          ),
          // Map
          Expanded(
            child: MapMetricsView(
              options: MapOptions(
                initCenter: Position(2.3522, 48.8566), // Paris (lng, lat)
                initZoom: 4.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: (style) {
                styleController = style;
                _addRegionLayer();
              },
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _addRegionLayer() async {
    // GeoJSON coordinates are always [lng, lat] — no conversion needed here,
    // this was never a LatLng literal.
    final geoJson = {
      'type': 'Feature',
      'properties': {},
      'geometry': {
        'type': 'Polygon',
        'coordinates': [
          [
            [-1.75, 43.3],
            [3.0, 43.3],
            [7.7, 43.8],
            [8.2, 48.9],
            [2.5, 51.1],
            [-4.8, 48.5],
            [-1.75, 43.3],
          ]
        ],
      },
    };

    await styleController?.addSource(
      GeoJsonSource(id: _sourceId, data: jsonEncode(geoJson)),
    );

    await styleController?.addLayer(
      FillStyleLayer(
        id: _fillLayerId,
        sourceId: _sourceId,
        paint: {'fill-color': currentColor, 'fill-opacity': 0.4},
      ),
    );

    await styleController?.addLayer(
      LineStyleLayer(
        id: _outlineLayerId,
        sourceId: _sourceId,
        paint: {'line-color': currentColor, 'line-width': 2.0},
      ),
    );
  }

  Future<void> _changeColor(String hexColor) async {
    setState(() {
      currentColor = hexColor;
    });

    // No setPaintProperty — remove and re-add each layer with the new color.
    await styleController?.removeLayer(_fillLayerId);
    await styleController?.removeLayer(_outlineLayerId);

    await styleController?.addLayer(
      FillStyleLayer(
        id: _fillLayerId,
        sourceId: _sourceId,
        paint: {'fill-color': hexColor, 'fill-opacity': 0.4},
      ),
    );
    await styleController?.addLayer(
      LineStyleLayer(
        id: _outlineLayerId,
        sourceId: _sourceId,
        paint: {'line-color': hexColor, 'line-width': 2.0},
      ),
    );
  }
}

Change Line Layer Color

Change the color of a route line dynamically:

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

class ChangeLineColorScreen extends StatefulWidget {
  @override
  _ChangeLineColorScreenState createState() => _ChangeLineColorScreenState();
}

const _sourceId = 'route';
const _layerId = 'route-line';

class _ChangeLineColorScreenState extends State<ChangeLineColorScreen> {
  MapController? mapController;
  StyleController? styleController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Change Line Color')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(5.0, 48.0),
              initZoom: 4.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: (style) {
              styleController = style;
              _addRouteLine();
            },
          ),
          // Floating color picker
          Positioned(
            bottom: 24,
            left: 16,
            right: 16,
            child: Container(
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(12),
                boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 6)],
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text('Route Color',
                      style: TextStyle(fontWeight: FontWeight.bold)),
                  SizedBox(height: 8),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      _colorCircle(Colors.blue, '#3b82f6'),
                      _colorCircle(Colors.red, '#ef4444'),
                      _colorCircle(Colors.green, '#22c55e'),
                      _colorCircle(Colors.purple, '#8b5cf6'),
                      _colorCircle(Colors.orange, '#f59e0b'),
                      _colorCircle(Colors.teal, '#14b8a6'),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _colorCircle(Color color, String hex) {
    return GestureDetector(
      onTap: () => _setRouteColor(hex),
      child: Container(
        width: 36,
        height: 36,
        decoration: BoxDecoration(
          color: color,
          shape: BoxShape.circle,
          border: Border.all(color: Colors.white, width: 2),
          boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2)],
        ),
      ),
    );
  }

  Future<void> _addRouteLine() async {
    final geoJson = {
      'type': 'Feature',
      'properties': {},
      'geometry': {
        'type': 'LineString',
        'coordinates': [
          [-3.7038, 40.4168],  // Madrid
          [2.349902, 48.853],  // Paris
          [13.405, 52.52],     // Berlin
          [16.3738, 48.2082],  // Vienna
          [12.4964, 41.9028],  // Rome
        ],
      },
    };

    await styleController?.addSource(
      GeoJsonSource(id: _sourceId, data: jsonEncode(geoJson)),
    );

    await styleController?.addLayer(
      const LineStyleLayer(
        id: _layerId,
        sourceId: _sourceId,
        paint: {'line-color': '#3b82f6', 'line-width': 4.0},
        layout: {'line-join': 'round', 'line-cap': 'round'},
      ),
    );
  }

  Future<void> _setRouteColor(String hex) async {
    // No setPaintProperty — remove and re-add with the new color.
    await styleController?.removeLayer(_layerId);
    await styleController?.addLayer(
      LineStyleLayer(
        id: _layerId,
        sourceId: _sourceId,
        paint: {'line-color': hex, 'line-width': 4.0},
        layout: const {'line-join': 'round', 'line-cap': 'round'},
      ),
    );
  }
}

Key Method

MethodDescription
StyleController.removeLayer(id) then addLayer(StyleLayer)Swap a layer you added for one with a new paint/layout map — there is no in-place setPaintProperty

Common paint properties (unchanged from the MapLibre style spec — passed as raw keys in the paint:/layout: maps):

Layer TypePropertyValues
Fillfill-colorHex color string
Fillfill-opacity0.0 to 1.0
Lineline-colorHex color string
Lineline-widthNumber (pixels)
Lineline-join / line-capLayout properties — go in layout:, not paint:
Circlecircle-colorHex color string
Circlecircle-radiusNumber (pixels)

Next Steps


Tip: Use the removeLayer + addLayer pattern for real-time theming — for example, re-declare every layer you own with new colors at once when the user switches between light and dark mode. This only works for layers you added yourself; layers baked into the base style JSON can't be re-declared this way (see Change Label Case for the same constraint).