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 Custom Icons with Markers in Flutter

Static pins render better with MarkerLayer

This page uses WidgetLayer, which repositions each marker in Dart every frame — it lags a frame while panning, ignores tilt and bearing, and does not scale past a few dozen markers. If your markers are plain pins with no Flutter content or gestures, use MarkerLayer instead; see Markers and Annotations.

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:

The SDK has no Marker/MarkerId/BitmapDescriptor/InfoWindow types and no markers: set on the map widget. Custom marker content is real Flutter widgets, placed with WidgetLayer in MapMetricsView.mapChildren. Each Marker there takes a point (Position), a size, and any child widget — so an asset image, a colored Icon, or a hand-built widget can all be used directly, without rasterizing to a bitmap first.

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> {
  MapController? mapController;
  List<Marker> markers = [];

  @override
  void initState() {
    super.initState();
    _loadCustomMarkers();
  }

  void _loadCustomMarkers() {
    Widget pin(String label) => GestureDetector(
      onTap: () => ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(label)),
      ),
      child: Image.asset('assets/icons/custom_pin.png', width: 48, height: 48),
    );

    setState(() {
      markers = [
        Marker(
          point: Position(2.3522, 48.8566), // Paris (lng, lat)
          size: const Size(48, 48),
          child: pin('Paris'),
        ),
        Marker(
          point: Position(-0.1276, 51.5074), // London
          size: const Size(48, 48),
          child: pin('London'),
        ),
        Marker(
          point: Position(13.405, 52.52), // Berlin
          size: const Size(48, 48),
          child: pin('Berlin'),
        ),
      ];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Custom Icon Markers')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: Position(5.0, 50.0),
          initZoom: 4.0,
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
        ),
        onMapCreated: (MapController controller) {
          mapController = controller;
        },
        mapChildren: [WidgetLayer(markers: markers, allowInteraction: true)],
      ),
    );
  }
}

Make sure to add your image to assets/icons/ and declare it in pubspec.yaml:

yaml
flutter:
  assets:
    - assets/icons/custom_pin.png

allowInteraction: true is required on WidgetLayer for the GestureDetector tap above to receive events — without it, the markers are rendered but transparent to touch so they don't block map panning.

Color-Coded Markers

Use a colored Icon widget per category instead of a bitmap hue helper:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class ColorCodedMarkersScreen extends StatefulWidget {
  @override
  _ColorCodedMarkersScreenState createState() =>
      _ColorCodedMarkersScreenState();
}

class _ColorCodedMarkersScreenState extends State<ColorCodedMarkersScreen> {
  MapController? mapController;

  Widget _pinIcon(Color color, String label) => GestureDetector(
    onTap: () => debugPrint(label),
    child: Icon(Icons.location_on, color: color, size: 36),
  );

  late final List<Marker> markers = [
    // Restaurants - Red markers
    Marker(
      point: Position(2.3422, 48.8566), // (lng, lat)
      size: const Size(36, 36),
      child: _pinIcon(Colors.red, 'Le Petit Bistro (Restaurant)'),
    ),
    Marker(
      point: Position(2.3500, 48.8600),
      size: const Size(36, 36),
      child: _pinIcon(Colors.red, 'Cafe de Flore (Restaurant)'),
    ),
    // Hotels - Blue markers
    Marker(
      point: Position(2.3300, 48.8650),
      size: const Size(36, 36),
      child: _pinIcon(Colors.blue, 'Grand Hotel (Hotel)'),
    ),
    Marker(
      point: Position(2.3600, 48.8530),
      size: const Size(36, 36),
      child: _pinIcon(Colors.blue, 'Hotel Rivoli (Hotel)'),
    ),
    // Attractions - Green markers
    Marker(
      point: Position(2.2945, 48.8584), // Eiffel Tower
      size: const Size(36, 36),
      child: _pinIcon(Colors.green, 'Eiffel Tower (Attraction)'),
    ),
    Marker(
      point: Position(2.3376, 48.8606), // Louvre Museum
      size: const Size(36, 36),
      child: _pinIcon(Colors.green, 'Louvre Museum (Attraction)'),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Color-Coded Markers')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(2.3422, 48.8566),
              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;
            },
            mapChildren: [WidgetLayer(markers: markers, allowInteraction: true)],
          ),
          // 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

Because Marker.child accepts any widget, "numbered pin" markers no longer need to be rendered to a bitmap with dart:ui — build the widget directly:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class WidgetMarkerScreen extends StatefulWidget {
  @override
  _WidgetMarkerScreenState createState() => _WidgetMarkerScreenState();
}

class _WidgetMarkerScreenState extends State<WidgetMarkerScreen> {
  MapController? mapController;
  List<Marker> markers = [];

  @override
  void initState() {
    super.initState();
    _createWidgetMarkers();
  }

  Widget _numberedIcon(int number, Color color) => Container(
    width: 32,
    height: 32,
    decoration: BoxDecoration(
      color: color,
      shape: BoxShape.circle,
      border: Border.all(color: Colors.white, width: 3),
    ),
    alignment: Alignment.center,
    child: Text(
      '$number',
      style: const TextStyle(
        color: Colors.white,
        fontSize: 14,
        fontWeight: FontWeight.bold,
      ),
    ),
  );

  void _createWidgetMarkers() {
    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},
    ];

    setState(() {
      markers = [
        for (final (i, loc) in locations.indexed)
          Marker(
            point: Position(loc['lng'] as double, loc['lat'] as double),
            size: const Size(32, 32),
            child: _numberedIcon(i + 1, Colors.blue),
          ),
      ];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Widget Markers')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: Position(2.3200, 48.8600),
          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;
        },
        mapChildren: [WidgetLayer(markers: markers)],
      ),
    );
  }
}

Marker Colors

There is no BitmapDescriptor.defaultMarkerWithHue helper — pass any Color straight to the widget you build for Marker.child (an Icon, a Container with BoxDecoration.color, etc.):

UseExample
Category colorIcon(Icons.location_on, color: Colors.red, size: 36)
Custom shape + colorContainer(decoration: BoxDecoration(color: Colors.blue, shape: BoxShape.circle))
Any Material colorColors.orange, Colors.purple, Colors.teal, ... — no fixed hue palette to pick from

Next Steps


Tip: For the best quality on high-DPI screens, export marker images at 2x or 3x resolution and size the Marker/Image.asset widget in logical pixels — Flutter handles the device pixel ratio for you, no ImageConfiguration(devicePixelRatio:) step required.