Add Custom Icons with Markers in Flutter
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
Custom Asset Icon Markers
Use a local image asset as a marker icon:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CustomIconMarkerScreen extends StatefulWidget {
@override
_CustomIconMarkerScreenState createState() => _CustomIconMarkerScreenState();
}
class _CustomIconMarkerScreenState extends State<CustomIconMarkerScreen> {
MapMetricsController? mapController;
Set<Marker> markers = {};
@override
void initState() {
super.initState();
_loadCustomMarkers();
}
Future<void> _loadCustomMarkers() async {
// Load a custom icon from assets
final customIcon = await BitmapDescriptor.fromAssetImage(
ImageConfiguration(size: Size(48, 48)),
'assets/icons/custom_pin.png',
);
setState(() {
markers = {
Marker(
markerId: MarkerId('paris'),
position: LatLng(48.8566, 2.3522),
icon: customIcon,
infoWindow: InfoWindow(title: 'Paris'),
),
Marker(
markerId: MarkerId('london'),
position: LatLng(51.5074, -0.1276),
icon: customIcon,
infoWindow: InfoWindow(title: 'London'),
),
Marker(
markerId: MarkerId('berlin'),
position: LatLng(52.52, 13.405),
icon: customIcon,
infoWindow: InfoWindow(title: 'Berlin'),
),
};
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Icon Markers')),
body: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(50.0, 5.0),
zoom: 4.0,
),
markers: markers,
),
);
}
}Make sure to add your image to assets/icons/ and declare it in pubspec.yaml:
yaml
flutter:
assets:
- assets/icons/custom_pin.pngColor-Coded Markers
Use different hue values of the default marker for categories:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class ColorCodedMarkersScreen extends StatefulWidget {
@override
_ColorCodedMarkersScreenState createState() =>
_ColorCodedMarkersScreenState();
}
class _ColorCodedMarkersScreenState extends State<ColorCodedMarkersScreen> {
MapMetricsController? mapController;
final Set<Marker> markers = {
// Restaurants - Red markers
Marker(
markerId: MarkerId('restaurant_1'),
position: LatLng(48.8566, 2.3422),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
infoWindow: InfoWindow(title: 'Le Petit Bistro', snippet: 'Restaurant'),
),
Marker(
markerId: MarkerId('restaurant_2'),
position: LatLng(48.8600, 2.3500),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
infoWindow: InfoWindow(title: 'Cafe de Flore', snippet: 'Restaurant'),
),
// Hotels - Blue markers
Marker(
markerId: MarkerId('hotel_1'),
position: LatLng(48.8650, 2.3300),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
infoWindow: InfoWindow(title: 'Grand Hotel', snippet: 'Hotel'),
),
Marker(
markerId: MarkerId('hotel_2'),
position: LatLng(48.8530, 2.3600),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
infoWindow: InfoWindow(title: 'Hotel Rivoli', snippet: 'Hotel'),
),
// Attractions - Green markers
Marker(
markerId: MarkerId('attraction_1'),
position: LatLng(48.8584, 2.2945),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
infoWindow: InfoWindow(title: 'Eiffel Tower', snippet: 'Attraction'),
),
Marker(
markerId: MarkerId('attraction_2'),
position: LatLng(48.8606, 2.3376),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
infoWindow: InfoWindow(title: 'Louvre Museum', snippet: 'Attraction'),
),
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Color-Coded Markers')),
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.8566, 2.3422),
zoom: 13.0,
),
markers: markers,
),
// 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
Create fully custom markers using Flutter widgets painted to a bitmap:
dart
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:mapmetrics/mapmetrics.dart';
class WidgetMarkerScreen extends StatefulWidget {
@override
_WidgetMarkerScreenState createState() => _WidgetMarkerScreenState();
}
class _WidgetMarkerScreenState extends State<WidgetMarkerScreen> {
MapMetricsController? mapController;
Set<Marker> markers = {};
@override
void initState() {
super.initState();
_createWidgetMarkers();
}
Future<BitmapDescriptor> _createNumberedIcon(int number, Color color) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final size = 48.0;
// Draw circle background
final paint = Paint()..color = color;
canvas.drawCircle(Offset(size / 2, size / 2), size / 2, paint);
// Draw white border
final borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawCircle(Offset(size / 2, size / 2), size / 2 - 1.5, borderPaint);
// Draw number text
final textPainter = TextPainter(
text: TextSpan(
text: '$number',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
(size - textPainter.width) / 2,
(size - textPainter.height) / 2,
),
);
final picture = recorder.endRecording();
final image = await picture.toImage(size.toInt(), size.toInt());
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
}
Future<void> _createWidgetMarkers() async {
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},
];
final markerSet = <Marker>{};
for (int i = 0; i < locations.length; i++) {
final icon = await _createNumberedIcon(i + 1, Colors.blue);
markerSet.add(
Marker(
markerId: MarkerId('stop_$i'),
position: LatLng(
locations[i]['lat'] as double,
locations[i]['lng'] as double,
),
icon: icon,
infoWindow: InfoWindow(title: locations[i]['name'] as String),
),
);
}
setState(() {
markers = markerSet;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Widget Markers')),
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.8600, 2.3200),
zoom: 13.0,
),
markers: markers,
),
);
}
}Available Marker Hue Constants
| Constant | Color |
|---|---|
BitmapDescriptor.hueRed | Red (0.0) |
BitmapDescriptor.hueOrange | Orange (30.0) |
BitmapDescriptor.hueYellow | Yellow (60.0) |
BitmapDescriptor.hueGreen | Green (120.0) |
BitmapDescriptor.hueCyan | Cyan (180.0) |
BitmapDescriptor.hueAzure | Azure (210.0) |
BitmapDescriptor.hueBlue | Blue (240.0) |
BitmapDescriptor.hueViolet | Violet (270.0) |
BitmapDescriptor.hueMagenta | Magenta (300.0) |
BitmapDescriptor.hueRose | Rose (330.0) |
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, provide marker images at 2x or 3x resolution and use ImageConfiguration(devicePixelRatio: 2.0) when loading from assets.