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. Images are registered with StyleController.addImage, and layers are added with StyleController.addLayer:
dart
import 'dart:convert';
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> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Icon to Map')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.3522, 48.8566), // lng, lat
initZoom: 12.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: (StyleController style) {
_addIconAndSymbolLayer(style);
},
),
);
}
Future<void> _addIconAndSymbolLayer(StyleController style) 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 style.addImage('custom-pin', imageData);
// Create a GeoJSON source with points (coordinates are [lng, lat])
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
await style.addSource(
GeoJsonSource(id: 'landmarks', data: jsonEncode(geoJson)),
);
await style.addLayer(
const SymbolStyleLayer(
id: 'landmarks-icons',
sourceId: 'landmarks',
layout: {
'icon-image': 'custom-pin',
'icon-size': 0.5,
'text-field': ['get', 'name'],
'text-size': 12.0,
'text-offset': [0.0, 1.5],
'text-anchor': '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:convert';
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> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Multiple Icons')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(2.3400, 48.8566), // lng, lat
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;
},
onStyleLoaded: (StyleController style) {
_addMultipleIcons(style);
},
),
// 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(StyleController style) 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');
// addImages batches all three into a single native call
await style.addImages({
'icon-restaurant': restaurantBytes.buffer.asUint8List(),
'icon-hotel': hotelBytes.buffer.asUint8List(),
'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],
},
},
],
};
await style.addSource(
GeoJsonSource(id: 'places', data: jsonEncode(geoJson)),
);
// Use a data-driven icon based on the 'icon' property.
// MapLibre style expressions — ['get', 'icon'] — read the value at
// render time; there is no `{icon}` string-token syntax in this SDK.
await style.addLayer(
const SymbolStyleLayer(
id: 'places-layer',
sourceId: 'places',
layout: {
'icon-image': ['get', 'icon'],
'icon-size': 0.4,
'text-field': ['get', 'name'],
'text-size': 11.0,
'text-offset': [0.0, 1.8],
'text-anchor': 'top',
},
paint: {'text-color': '#333333'},
),
);
}
}Generate Icon from Flutter Widget
Create an icon programmatically using Canvas drawing:
dart
import 'dart:convert';
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> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Generated Icon')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.3400, 48.8566), // lng, lat
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;
},
onStyleLoaded: (StyleController style) {
_addGeneratedIcons(style);
},
),
);
}
/// 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(StyleController style) 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 style.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],
},
});
}
await style.addSource(
GeoJsonSource(
id: 'generated-icons',
data: jsonEncode({
'type': 'FeatureCollection',
'features': features,
}),
),
);
await style.addLayer(
const SymbolStyleLayer(
id: 'generated-icons-layer',
sourceId: 'generated-icons',
layout: {
'icon-image': ['get', 'iconId'],
'icon-size': 0.6,
'text-field': ['get', 'name'],
'text-size': 12.0,
'text-offset': [0.0, 2.0],
'text-anchor': '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 'icon-image' to a MapLibre expression like ['get', 'propertyName'] — the map engine evaluates it per feature at render time. This lets you use different icons for different categories from a single layer.