Add Custom Icons with Markers in Flutter
Static pins render better with MarkerLayer
This page uses WidgetLayer, which repositions each marker in Dart every frame — it lags a frame while panning, ignores tilt and bearing, and does not scale past a few dozen markers. If your markers are plain pins with no Flutter content or gestures, use MarkerLayer instead; see Markers and Annotations.
This tutorial shows how to create markers with custom icons — using asset images, network images, or Flutter widgets — instead of the default pin.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
The SDK has no
Marker/MarkerId/BitmapDescriptor/InfoWindowtypes and nomarkers:set on the map widget. Custom marker content is real Flutter widgets, placed withWidgetLayerinMapMetricsView.mapChildren. EachMarkerthere takes apoint(Position), asize, and anychildwidget — so an asset image, a coloredIcon, or a hand-built widget can all be used directly, without rasterizing to a bitmap first.
Custom Asset Icon Markers
Use a local image asset as a marker icon:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CustomIconMarkerScreen extends StatefulWidget {
@override
_CustomIconMarkerScreenState createState() => _CustomIconMarkerScreenState();
}
class _CustomIconMarkerScreenState extends State<CustomIconMarkerScreen> {
MapController? mapController;
List<Marker> markers = [];
@override
void initState() {
super.initState();
_loadCustomMarkers();
}
void _loadCustomMarkers() {
Widget pin(String label) => GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(label)),
),
child: Image.asset('assets/icons/custom_pin.png', width: 48, height: 48),
);
setState(() {
markers = [
Marker(
point: Position(2.3522, 48.8566), // Paris (lng, lat)
size: const Size(48, 48),
child: pin('Paris'),
),
Marker(
point: Position(-0.1276, 51.5074), // London
size: const Size(48, 48),
child: pin('London'),
),
Marker(
point: Position(13.405, 52.52), // Berlin
size: const Size(48, 48),
child: pin('Berlin'),
),
];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Icon Markers')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(5.0, 50.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;
},
mapChildren: [WidgetLayer(markers: markers, allowInteraction: true)],
),
);
}
}Make sure to add your image to assets/icons/ and declare it in pubspec.yaml:
flutter:
assets:
- assets/icons/custom_pin.pngallowInteraction: true is required on WidgetLayer for the GestureDetector tap above to receive events — without it, the markers are rendered but transparent to touch so they don't block map panning.
Color-Coded Markers
Use a colored Icon widget per category instead of a bitmap hue helper:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class ColorCodedMarkersScreen extends StatefulWidget {
@override
_ColorCodedMarkersScreenState createState() =>
_ColorCodedMarkersScreenState();
}
class _ColorCodedMarkersScreenState extends State<ColorCodedMarkersScreen> {
MapController? mapController;
Widget _pinIcon(Color color, String label) => GestureDetector(
onTap: () => debugPrint(label),
child: Icon(Icons.location_on, color: color, size: 36),
);
late final List<Marker> markers = [
// Restaurants - Red markers
Marker(
point: Position(2.3422, 48.8566), // (lng, lat)
size: const Size(36, 36),
child: _pinIcon(Colors.red, 'Le Petit Bistro (Restaurant)'),
),
Marker(
point: Position(2.3500, 48.8600),
size: const Size(36, 36),
child: _pinIcon(Colors.red, 'Cafe de Flore (Restaurant)'),
),
// Hotels - Blue markers
Marker(
point: Position(2.3300, 48.8650),
size: const Size(36, 36),
child: _pinIcon(Colors.blue, 'Grand Hotel (Hotel)'),
),
Marker(
point: Position(2.3600, 48.8530),
size: const Size(36, 36),
child: _pinIcon(Colors.blue, 'Hotel Rivoli (Hotel)'),
),
// Attractions - Green markers
Marker(
point: Position(2.2945, 48.8584), // Eiffel Tower
size: const Size(36, 36),
child: _pinIcon(Colors.green, 'Eiffel Tower (Attraction)'),
),
Marker(
point: Position(2.3376, 48.8606), // Louvre Museum
size: const Size(36, 36),
child: _pinIcon(Colors.green, 'Louvre Museum (Attraction)'),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Color-Coded Markers')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(2.3422, 48.8566),
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
mapChildren: [WidgetLayer(markers: markers, allowInteraction: true)],
),
// Legend
Positioned(
top: 16,
left: 16,
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_legendItem(Colors.red, 'Restaurants'),
_legendItem(Colors.blue, 'Hotels'),
_legendItem(Colors.green, 'Attractions'),
],
),
),
),
],
),
);
}
Widget _legendItem(Color color, String label) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.location_on, color: color, size: 18),
SizedBox(width: 4),
Text(label, style: TextStyle(fontSize: 13)),
],
),
);
}
}Custom Widget Markers
Because Marker.child accepts any widget, "numbered pin" markers no longer need to be rendered to a bitmap with dart:ui — build the widget directly:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class WidgetMarkerScreen extends StatefulWidget {
@override
_WidgetMarkerScreenState createState() => _WidgetMarkerScreenState();
}
class _WidgetMarkerScreenState extends State<WidgetMarkerScreen> {
MapController? mapController;
List<Marker> markers = [];
@override
void initState() {
super.initState();
_createWidgetMarkers();
}
Widget _numberedIcon(int number, Color color) => Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
),
alignment: Alignment.center,
child: Text(
'$number',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
);
void _createWidgetMarkers() {
final locations = [
{'name': 'Stop 1: Eiffel Tower', 'lat': 48.8584, 'lng': 2.2945},
{'name': 'Stop 2: Louvre', 'lat': 48.8606, 'lng': 2.3376},
{'name': 'Stop 3: Notre-Dame', 'lat': 48.8530, 'lng': 2.3499},
{'name': 'Stop 4: Sacre-Coeur', 'lat': 48.8867, 'lng': 2.3431},
];
setState(() {
markers = [
for (final (i, loc) in locations.indexed)
Marker(
point: Position(loc['lng'] as double, loc['lat'] as double),
size: const Size(32, 32),
child: _numberedIcon(i + 1, Colors.blue),
),
];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Widget Markers')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.3200, 48.8600),
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
mapChildren: [WidgetLayer(markers: markers)],
),
);
}
}Marker Colors
There is no BitmapDescriptor.defaultMarkerWithHue helper — pass any Color straight to the widget you build for Marker.child (an Icon, a Container with BoxDecoration.color, etc.):
| Use | Example |
|---|---|
| Category color | Icon(Icons.location_on, color: Colors.red, size: 36) |
| Custom shape + color | Container(decoration: BoxDecoration(color: Colors.blue, shape: BoxShape.circle)) |
| Any Material color | Colors.orange, Colors.purple, Colors.teal, ... — no fixed hue palette to pick from |
Next Steps
- Add Image Markers — Use network images as markers
- Draggable Marker — Make markers draggable
- Filter Markers — Show/hide markers by category
Tip: For the best quality on high-DPI screens, export marker images at 2x or 3x resolution and size the Marker/Image.asset widget in logical pixels — Flutter handles the device pixel ratio for you, no ImageConfiguration(devicePixelRatio:) step required.