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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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:
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:
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
| Method | Description |
|---|---|
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 Type | Property | Values |
|---|---|---|
| Fill | fill-color | Hex color string |
| Fill | fill-opacity | 0.0 to 1.0 |
| Line | line-color | Hex color string |
| Line | line-width | Number (pixels) |
| Line | line-join / line-cap | Layout properties — go in layout:, not paint: |
| Circle | circle-color | Hex color string |
| Circle | circle-radius | Number (pixels) |
Next Steps
- Custom Map Styling — Apply full custom styles
- Add a GeoJSON Polygon — Draw filled areas
- Toggle Interactions — Control map behavior
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).