Skip to content

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:

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

Color-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

ConstantColor
BitmapDescriptor.hueRedRed (0.0)
BitmapDescriptor.hueOrangeOrange (30.0)
BitmapDescriptor.hueYellowYellow (60.0)
BitmapDescriptor.hueGreenGreen (120.0)
BitmapDescriptor.hueCyanCyan (180.0)
BitmapDescriptor.hueAzureAzure (210.0)
BitmapDescriptor.hueBlueBlue (240.0)
BitmapDescriptor.hueVioletViolet (270.0)
BitmapDescriptor.hueMagentaMagenta (300.0)
BitmapDescriptor.hueRoseRose (330.0)

Next Steps


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.