Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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:

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.png

Multiple 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 ​


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.