Skip to content

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:

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

Multiple 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


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.