Add an Icon to the Map in Flutter
This tutorial shows how to add custom icon images to the map style and use them as symbols on markers or layers.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Add Asset Image as Map Icon
Load a local asset image and add it to the map style for use in symbol layers:
dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mapmetrics/mapmetrics.dart';
class AddIconToMapScreen extends StatefulWidget {
@override
_AddIconToMapScreenState createState() => _AddIconToMapScreenState();
}
class _AddIconToMapScreenState extends State<AddIconToMapScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Icon to Map')),
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.8566, 2.3522),
zoom: 12.0,
),
onStyleLoaded: () {
_addIconAndSymbolLayer();
},
),
);
}
Future<void> _addIconAndSymbolLayer() async {
// Load the icon image from assets
final ByteData bytes = await rootBundle.load('assets/icons/pin.png');
final Uint8List imageData = bytes.buffer.asUint8List();
// Add the image to the map style
await mapController?.addImage('custom-pin', imageData);
// Create a GeoJSON source with points
final geoJson = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {'name': 'Eiffel Tower'},
'geometry': {
'type': 'Point',
'coordinates': [2.2945, 48.8584],
},
},
{
'type': 'Feature',
'properties': {'name': 'Louvre Museum'},
'geometry': {
'type': 'Point',
'coordinates': [2.3376, 48.8606],
},
},
{
'type': 'Feature',
'properties': {'name': 'Notre-Dame'},
'geometry': {
'type': 'Point',
'coordinates': [2.3499, 48.8530],
},
},
],
};
// Add source and symbol layer using the custom icon
mapController?.addGeoJsonSource('landmarks', geoJson);
mapController?.addSymbolLayer(
'landmarks-icons',
'landmarks',
iconImage: 'custom-pin',
iconSize: 0.5,
textField: '{name}',
textSize: 12.0,
textOffset: [0.0, 1.5],
textAnchor: 'top',
);
}
}Make sure to declare the asset in pubspec.yaml:
yaml
flutter:
assets:
- assets/icons/pin.pngMultiple Icon Types
Add different icons for different place categories:
dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mapmetrics/mapmetrics.dart';
class MultiIconScreen extends StatefulWidget {
@override
_MultiIconScreenState createState() => _MultiIconScreenState();
}
class _MultiIconScreenState extends State<MultiIconScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Multiple Icons')),
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.3400),
zoom: 13.0,
),
onStyleLoaded: () {
_addMultipleIcons();
},
),
// Legend
Positioned(
bottom: 16,
left: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Legend',
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 4),
_legendRow(Icons.restaurant, Colors.red, 'Restaurants'),
_legendRow(Icons.hotel, Colors.blue, 'Hotels'),
_legendRow(Icons.museum, Colors.green, 'Museums'),
],
),
),
),
),
],
),
);
}
Widget _legendRow(IconData icon, Color color, String label) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 18),
SizedBox(width: 6),
Text(label, style: TextStyle(fontSize: 13)),
],
),
);
}
Future<void> _addMultipleIcons() async {
// Load different icon assets
final restaurantBytes =
await rootBundle.load('assets/icons/restaurant.png');
final hotelBytes = await rootBundle.load('assets/icons/hotel.png');
final museumBytes = await rootBundle.load('assets/icons/museum.png');
await mapController?.addImage(
'icon-restaurant', restaurantBytes.buffer.asUint8List());
await mapController?.addImage(
'icon-hotel', hotelBytes.buffer.asUint8List());
await mapController?.addImage(
'icon-museum', museumBytes.buffer.asUint8List());
final geoJson = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {'name': 'Le Jules Verne', 'icon': 'icon-restaurant'},
'geometry': {
'type': 'Point',
'coordinates': [2.2945, 48.8580],
},
},
{
'type': 'Feature',
'properties': {'name': 'Hotel Ritz', 'icon': 'icon-hotel'},
'geometry': {
'type': 'Point',
'coordinates': [2.3285, 48.8682],
},
},
{
'type': 'Feature',
'properties': {'name': 'Louvre Museum', 'icon': 'icon-museum'},
'geometry': {
'type': 'Point',
'coordinates': [2.3376, 48.8606],
},
},
],
};
mapController?.addGeoJsonSource('places', geoJson);
// Use data-driven icon based on the 'icon' property
mapController?.addSymbolLayer(
'places-layer',
'places',
iconImage: '{icon}', // References the 'icon' property in GeoJSON
iconSize: 0.4,
textField: '{name}',
textSize: 11.0,
textOffset: [0.0, 1.8],
textAnchor: 'top',
textColor: '#333333',
);
}
}Generate Icon from Flutter Widget
Create an icon programmatically using Canvas drawing:
dart
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class GeneratedIconScreen extends StatefulWidget {
@override
_GeneratedIconScreenState createState() => _GeneratedIconScreenState();
}
class _GeneratedIconScreenState extends State<GeneratedIconScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Generated Icon')),
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.8566, 2.3400),
zoom: 13.0,
),
onStyleLoaded: () {
_addGeneratedIcons();
},
),
);
}
/// Draw a colored circle icon with a label
Future<Uint8List> _generateCircleIcon(
Color color, String label, double size) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// Draw filled circle
final paint = Paint()..color = color;
canvas.drawCircle(Offset(size / 2, size / 2), size / 2, paint);
// Draw 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 label
final textPainter = TextPainter(
text: TextSpan(
text: label,
style: TextStyle(
color: Colors.white,
fontSize: size * 0.35,
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 bytes!.buffer.asUint8List();
}
Future<void> _addGeneratedIcons() async {
// Generate numbered icons
final colors = [Colors.blue, Colors.red, Colors.green];
final labels = ['1', '2', '3'];
final positions = [
[2.2945, 48.8584],
[2.3376, 48.8606],
[2.3499, 48.8530],
];
final names = ['Eiffel Tower', 'Louvre', 'Notre-Dame'];
for (int i = 0; i < 3; i++) {
final iconData = await _generateCircleIcon(colors[i], labels[i], 64);
await mapController?.addImage('gen-icon-$i', iconData);
}
final features = <Map<String, dynamic>>[];
for (int i = 0; i < positions.length; i++) {
features.add({
'type': 'Feature',
'properties': {'name': names[i], 'iconId': 'gen-icon-$i'},
'geometry': {
'type': 'Point',
'coordinates': positions[i],
},
});
}
mapController?.addGeoJsonSource('generated-icons', {
'type': 'FeatureCollection',
'features': features,
});
mapController?.addSymbolLayer(
'generated-icons-layer',
'generated-icons',
iconImage: '{iconId}',
iconSize: 0.6,
textField: '{name}',
textSize: 12.0,
textOffset: [0.0, 2.0],
textAnchor: 'top',
);
}
}Next Steps
- Add Custom Icons with Markers — Use custom marker icons
- Add Image Markers — Network image markers
- Draw GeoJSON Points — Circle-based point rendering
Tip: For data-driven icons, set the iconImage property to '{propertyName}' — the map engine will look up the icon name from each feature's properties. This lets you use different icons for different categories from a single layer.