Add Clusters in Flutter
This tutorial shows how to group nearby markers into clusters for better performance and readability when you have many data points on the map.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Clustering
When you have many markers close together, clustering groups them into a single icon showing the count. As the user zooms in, clusters break apart into individual markers:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
import 'dart:math';
class ClusterExampleScreen extends StatefulWidget {
@override
_ClusterExampleScreenState createState() => _ClusterExampleScreenState();
}
class _ClusterExampleScreenState extends State<ClusterExampleScreen> {
MapMetricsController? mapController;
Set<Marker> markers = {};
double currentZoom = 12.0;
// Sample data: 100 random points around Paris
late final List<LatLng> allPoints;
@override
void initState() {
super.initState();
final random = Random(42);
allPoints = List.generate(100, (_) {
return LatLng(
48.82 + random.nextDouble() * 0.08, // Lat range around Paris
2.28 + random.nextDouble() * 0.14, // Lng range around Paris
);
});
_updateMarkers();
}
void _updateMarkers() {
// Simple clustering: group nearby points based on zoom level
final double gridSize = 0.01 * pow(2, 15 - currentZoom).toDouble();
final Map<String, List<LatLng>> grid = {};
for (final point in allPoints) {
final key =
'${(point.latitude / gridSize).floor()}_${(point.longitude / gridSize).floor()}';
grid.putIfAbsent(key, () => []).add(point);
}
final Set<Marker> newMarkers = {};
for (final entry in grid.entries) {
final points = entry.value;
// Calculate center of the group
final avgLat = points.map((p) => p.latitude).reduce((a, b) => a + b) / points.length;
final avgLng = points.map((p) => p.longitude).reduce((a, b) => a + b) / points.length;
final center = LatLng(avgLat, avgLng);
if (points.length == 1) {
// Single point — show as regular marker
newMarkers.add(
Marker(
markerId: MarkerId(entry.key),
position: center,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
infoWindow: InfoWindow(
title: 'Point',
snippet:
'${center.latitude.toStringAsFixed(4)}, ${center.longitude.toStringAsFixed(4)}',
),
),
);
} else {
// Cluster — show count
newMarkers.add(
Marker(
markerId: MarkerId('cluster_${entry.key}'),
position: center,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
infoWindow: InfoWindow(
title: '${points.length} points',
snippet: 'Zoom in to see details',
),
),
);
}
}
setState(() {
markers = newMarkers;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Clustered Markers')),
body: Column(
children: [
Container(
padding: EdgeInsets.all(10),
color: Colors.grey[100],
child: Text(
'${allPoints.length} total points | ${markers.length} visible markers | Zoom: ${currentZoom.toStringAsFixed(1)}',
style: TextStyle(fontSize: 13),
),
),
Expanded(
child: MapMetrics(
styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (controller) => mapController = controller,
onCameraIdle: () async {
final position = await mapController?.getCameraPosition();
if (position != null && position.zoom != currentZoom) {
currentZoom = position.zoom;
_updateMarkers();
}
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8566, 2.3522),
zoom: 12.0,
),
markers: markers,
),
),
],
),
);
}
}Cluster with Custom Widget Icons
Create visually distinct cluster icons that show the count and change color based on size:
dart
Future<BitmapDescriptor> _createClusterIcon(int count) async {
Color color;
double size;
if (count < 10) {
color = Colors.blue;
size = 40;
} else if (count < 50) {
color = Colors.orange;
size = 50;
} else {
color = Colors.red;
size = 60;
}
return await BitmapDescriptor.fromWidget(
Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color.withOpacity(0.8),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
),
alignment: Alignment.center,
child: Text(
'$count',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: size * 0.35,
),
),
),
);
}Tap Cluster to Zoom In
Zoom into a cluster when tapped:
dart
Marker(
markerId: MarkerId('cluster_${entry.key}'),
position: center,
icon: clusterIcon,
onTap: () {
// Zoom in to break apart the cluster
mapController?.animateCamera(
CameraUpdate.newLatLngZoom(center, currentZoom + 2),
);
},
)Cluster Size Colors
| Count | Color | Size |
|---|---|---|
| 1–9 | Blue | Small (40px) |
| 10–49 | Orange | Medium (50px) |
| 50+ | Red | Large (60px) |
Next Steps
- Add a Heatmap — Density visualization alternative to clusters
- Markers and Annotations — Basic marker features
- Fit to Bounding Box — Zoom to show all clusters
Tip: Recalculate clusters on onCameraIdle (not onCameraMove) to avoid excessive recomputation during panning.