Adding & Customising Markers
Adding & Customising Markers
A marker is the primary way to highlight a specific geographic point on a Google Map in Flutter. The google_maps_flutter package provides the Marker class, which lets you place pins at any latitude/longitude, give them custom icons, attach human-readable titles and snippets, and respond to user taps via callbacks. In a real application you will almost always manage a dynamic set of markers stored in widget state so that you can add, remove, or update them at runtime.
The Marker Class — Core Properties
Every Marker requires a unique MarkerId and a position. All other properties are optional:
- markerId — a
MarkerId(String value)that uniquely identifies the marker within the map. - position — a
LatLng(lat, lng)specifying where the pin appears. - infoWindow — an
InfoWindowwith atitleand optionalsnippetshown when the marker is tapped. - icon — a
BitmapDescriptor; defaults to the red Google pin. Swap in a custom asset or a hue-shifted version with the factory constructors. - onTap — a
VoidCallbackfired whenever the user taps the marker. - draggable — when
true, the user can long-press and drag the marker; combine withonDragEndto capture the new position. - visible — toggle marker visibility without removing it from the set.
- alpha — opacity between 0.0 (transparent) and 1.0 (opaque).
- zIndex — draw order when markers overlap.
Storing Markers in Widget State
The GoogleMap widget accepts a Set<Marker> via its markers parameter. Because Set uses identity for equality, you must replace the entire set (or use setState) to trigger a rebuild whenever you add or remove a marker. The idiomatic pattern is to keep a Set<Marker> field in your State class.
Example 1 — Basic StatefulWidget with a Dynamic Marker Set
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MarkerDemoMap extends StatefulWidget {
const MarkerDemoMap({super.key});
@override
State<MarkerDemoMap> createState() => _MarkerDemoMapState();
}
class _MarkerDemoMapState extends State<MarkerDemoMap> {
// All active markers live here; GoogleMap reads this set on every build.
final Set<Marker> _markers = {};
int _markerCounter = 0;
static const CameraPosition _initialCamera = CameraPosition(
target: LatLng(25.2048, 55.2708), // Dubai
zoom: 12,
);
/// Adds a new marker at [position] with an auto-generated ID.
void _addMarker(LatLng position) {
_markerCounter++;
final id = MarkerId('marker_$_markerCounter');
final marker = Marker(
markerId: id,
position: position,
infoWindow: InfoWindow(
title: 'Location #$_markerCounter',
snippet: '${position.latitude.toStringAsFixed(4)}, '
'${position.longitude.toStringAsFixed(4)}',
),
onTap: () => _onMarkerTapped(id),
);
setState(() {
_markers.add(marker);
});
}
void _onMarkerTapped(MarkerId id) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped marker: ${id.value}')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Marker Demo')),
body: GoogleMap(
initialCameraPosition: _initialCamera,
markers: _markers,
onLongPress: _addMarker, // long-press anywhere to drop a pin
),
);
}
}
Set<Marker> compares markers by their MarkerId. If you call _markers.add() with a marker whose ID already exists in the set, it will not replace the old one — the set silently ignores the duplicate. To update an existing marker, remove the old one first and then add the replacement, or rebuild the whole set.Custom Marker Icons with BitmapDescriptor
Flutter's BitmapDescriptor provides several factory constructors for custom icons:
BitmapDescriptor.defaultMarker— the default red pin.BitmapDescriptor.defaultMarkerWithHue(hue)— recolour the standard pin; hue is a double 0–360 (e.g.BitmapDescriptor.hueBlue).BitmapDescriptor.fromAssetImage(config, assetPath)— load a PNG from your asset bundle at the correct device pixel ratio (async).BitmapDescriptor.fromBytes(bytes)— supply raw PNG bytes; useful when generating icons at runtime.
Example 2 — Custom Icons and InfoWindow with onTap
class CustomIconMap extends StatefulWidget {
const CustomIconMap({super.key});
@override
State<CustomIconMap> createState() => _CustomIconMapState();
}
class _CustomIconMapState extends State<CustomIconMap> {
final Set<Marker> _markers = {};
BitmapDescriptor? _cafeIcon;
BitmapDescriptor? _hotelIcon;
@override
void initState() {
super.initState();
_loadIcons();
}
Future<void> _loadIcons() async {
// Load custom PNG assets (must be declared in pubspec.yaml)
final config = createLocalImageConfiguration(context);
final cafe = await BitmapDescriptor.fromAssetImage(
config,
'assets/icons/cafe_pin.png',
);
final hotel = await BitmapDescriptor.fromAssetImage(
config,
'assets/icons/hotel_pin.png',
);
setState(() {
_cafeIcon = cafe;
_hotelIcon = hotel;
_buildInitialMarkers();
});
}
void _buildInitialMarkers() {
final List<Map<String, dynamic>> places = [
{
'id': 'cafe_1',
'name': 'Sunrise Cafe',
'snippet': 'Open 07:00 – 22:00',
'lat': 25.2048,
'lng': 55.2708,
'type': 'cafe',
},
{
'id': 'hotel_1',
'name': 'Grand Tower Hotel',
'snippet': '5-star · Free WiFi',
'lat': 25.1972,
'lng': 55.2796,
'type': 'hotel',
},
];
for (final p in places) {
_markers.add(Marker(
markerId: MarkerId(p['id'] as String),
position: LatLng(p['lat'] as double, p['lng'] as double),
icon: p['type'] == 'cafe'
? (_cafeIcon ?? BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueOrange))
: (_hotelIcon ?? BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueBlue)),
infoWindow: InfoWindow(
title: p['name'] as String,
snippet: p['snippet'] as String,
onTap: () {
// InfoWindow tap — e.g. open a detail sheet
debugPrint('InfoWindow tapped for ${p['id']}');
},
),
onTap: () {
// Marker pin tap (fires before InfoWindow opens)
debugPrint('Marker pin tapped: ${p['id']}');
},
));
}
}
@override
Widget build(BuildContext context) {
return GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(25.2048, 55.2708),
zoom: 13,
),
markers: _markers,
);
}
}
initState (or lazily before the map renders) so the correct icon is already available when the first build call happens. If you load them asynchronously after the map is already shown, make sure to call setState after the await so the new icons are applied.Removing and Replacing Markers
To remove a marker by its MarkerId, use removeWhere on the set inside setState:
_markers.removeWhere((m) => m.markerId == targetId);- To replace a marker, call
removeWherefirst, thenaddthe updated version. - To clear all markers:
_markers.clear();
setState callback. Mutating directly without setState changes the data in memory but does not trigger a rebuild, so the map will display stale pins until the next unrelated rebuild. Always wrap marker set changes in setState(() { ... });.Best Practices for Production Marker Management
- Keep marker IDs stable and meaningful (e.g. use a database row ID as the suffix) so you can identify and update them reliably.
- For large datasets (>500 markers), consider clustering with the
google_maps_cluster_managerpackage to avoid performance issues. - Pre-load all
BitmapDescriptorinstances once (e.g. in a service orinitState) rather than creating them on everysetStatecall. - Use the
draggable: true+onDragEndcallback pair when you need user-repositionable pins.
Summary
In this lesson you learned how to create Marker objects with titles, snippets, custom icons, and tap callbacks, and how to store them in a Set<Marker> held in widget state. Calling setState whenever you add, remove, or replace a marker ensures the GoogleMap widget re-renders with the latest pins. With these fundamentals in place you can build rich, interactive map experiences — from simple point-of-interest overlays to fully dynamic, data-driven pin layers.