Integrating Google Maps with Marker Clustering in Flutter
Google Maps integration is a cornerstone for many mobile applications, providing location-based services and interactive geographical data. When dealing with a large number of markers on a map, performance degradation and UI clutter become significant issues. Marker clustering offers an elegant solution by grouping nearby markers into a single, interactive cluster icon, which expands into individual markers upon zooming in. This article will guide you through integrating Google Maps with marker clustering capabilities in your Flutter application.
Prerequisites
Before diving into the implementation, ensure you have the following:
- Flutter SDK installed and configured.
- A Google Cloud Project with the Maps SDK for Android and iOS APIs enabled.
- An API Key generated from your Google Cloud Project.
Setting Up Google Maps in Flutter
First, add the necessary dependency to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
google_maps_flutter: ^2.5.0 # Use the latest version
google_maps_cluster_manager: ^3.0.0 # Use the latest version for clustering
Next, configure your Android and iOS projects to use the Google Maps API key.
Android Configuration
Add your API key to the <application> tag in android/app/src/main/AndroidManifest.xml:
<manifest ...>
<application ...>
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY"/>
<!-- Other activities and services -->
</application>
</manifest>
iOS Configuration
Add your API key to ios/Runner/AppDelegate.swift:
import UIKit
import Flutter
import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR_API_KEY")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Implementing Marker Clustering
We'll use the google_maps_cluster_manager package, which provides an efficient way to manage and display clusters on google_maps_flutter.
1. Define a Custom Cluster Item
Your markers need to extend the ClusterItem class. This class typically holds the geographical coordinates (LatLng) of the item.
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
class Place with ClusterItem {
final String id;
final LatLng latLng;
final String name; // Optional: additional data for your place
Place({required this.id, required this.latLng, required this.name});
@override
LatLng get location => latLng;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Place && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
2. Integrate Cluster Manager with Google Map
Now, let's set up our Flutter widget to display the map with clustering. This involves initializing ClusterManager, providing marker and cluster builders, and updating the map as the camera moves.
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
import 'dart:math';
import 'dart:async';
import 'dart:ui' as ui; // For ImageDescriptor
class MapWithClusteringPage extends StatefulWidget {
const MapWithClusteringPage({super.key});
@override
State<MapWithClusteringPage> createState() => _MapWithClusteringPageState();
}
class _MapWithClusteringPageState extends State<MapWithClusteringPage> {
GoogleMapController? _mapController;
Set<Marker> markers = {};
late ClusterManager _manager;
// Initial camera position for the map
final CameraPosition _initialCameraPosition = const CameraPosition(
target: LatLng(37.7749, -122.4194), // San Francisco
zoom: 10.0,
);
// List of dummy places
final List<Place> items = [
Place(id: '1', latLng: const LatLng(37.7749, -122.4194), name: 'Place 1'),
Place(id: '2', latLng: const LatLng(37.7750, -122.4190), name: 'Place 2'),
Place(id: '3', latLng: const LatLng(37.7760, -122.4180), name: 'Place 3'),
Place(id: '4', latLng: const LatLng(37.7770, -122.4170), name: 'Place 4'),
Place(id: '5', latLng: const LatLng(37.7780, -122.4160), name: 'Place 5'),
Place(id: '6', latLng: const LatLng(37.7700, -122.4000), name: 'Place 6'),
Place(id: '7', latLng: const LatLng(37.7800, -122.4200), name: 'Place 7'),
Place(id: '8', latLng: const LatLng(37.7700, -122.4300), name: 'Place 8'),
Place(id: '9', latLng: const LatLng(37.7600, -122.4100), name: 'Place 9'),
Place(id: '10', latLng: const LatLng(37.7850, -122.4050), name: 'Place 10'),
// Add more dummy data
for (int i = 11; i < 100; i++)
Place(
id: '$i',
latLng: LatLng(
37.7 + (Random().nextDouble() - 0.5) * 0.2, // Random latitude around SF
-122.4 + (Random().nextDouble() - 0.5) * 0.4, // Random longitude around SF
),
name: 'Place $i',
),
];
@override
void initState() {
_manager = _initClusterManager();
super.initState();
}
ClusterManager _initClusterManager() {
return ClusterManager<Place>(items, _updateMarkers,
markerBuilder: _markerBuilder,
stopClusteringZoom: 17.0 // Stop clustering after this zoom level
);
}
void _onMapCreated(GoogleMapController controller) {
_mapController = controller;
_manager.set;'mapController', _mapController;
_manager.updateMap();
}
// This method updates the set of markers displayed on the map
void _updateMarkers(Set<Marker> newMarkers) {
setState(() {
markers = newMarkers;
});
}
// Builder for individual markers and cluster markers
Future<Marker> Function(Cluster<Place>) get _markerBuilder =>
(cluster) async {
return Marker(
markerId: MarkerId(cluster.id),
position: cluster.location,
onTap: () {
if (cluster.is}ge) {
// Zoom in if it's a cluster
_mapController?.animateCamera(
CameraUpdate.newLatLngZoom(
cluster.location,
_mapController!.camera.zoom + 2, // Zoom in by 2 levels
),
);
} else {
// Handle tap on individual marker
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Tapped on ${cluster.items.first.name}'),
duration: const Duration(seconds: 1),
),
);
}
},
icon: await _getClusterBitmap(cluster.isGeocluster ? cluster.count : 0,
cluster.isGeocluster ? 125 : 75), // Adjust size based on if it's a cluster
);
};
// Helper to create a bitmap for the marker icon
Future<BitmapDescriptor> _getClusterBitmap(int size, {int textScale = 0}) async {
if (size == 0) { // For individual markers
return BitmapDescriptor.defaultMarker; // Or a custom image asset
}
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
final Canvas canvas = Canvas(pictureRecorder);
final Paint paint = Paint()..color = Colors.blue.withOpacity(0.7);
final TextPainter textPainter = TextPainter(
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
);
// Draw circle for cluster
canvas.drawCircle(Offset(textScale / 2, textScale / 2), textScale / 2, paint);
// Draw text (count) inside the circle
textPainter.text = TextSpan(
text: size.toString(),
style: TextStyle(
fontSize: textScale / 3, // Adjust font size based on circle size
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
(textScale / 2) - (textPainter.width / 2),
(textScale / 2) - (textPainter.height / 2),
),
);
final img = await pictureRecorder.endRecording().toImage(textScale, textScale);
final data = await img.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Google Maps with Clustering'),
backgroundColor: Colors.blueAccent,
),
body: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _initialCameraPosition,
onMapCreated: _onMapCreated,
markers: markers,
onCameraIdle: _manager.updateMap, // Trigger clustering update when camera stops moving
onCameraMove: _manager.onCameraMove, // Pass camera movement to manager
),
);
}
}
Explanation of Key Components:
ClusterManager<Place>(items, _updateMarkers, markerBuilder: _markerBuilder):items: A list of your customPlaceobjects (which extendClusterItem)._updateMarkers: A callback function that the cluster manager invokes whenever the set of visible markers (individual or clustered) changes. You update your map'smarkersstate here.markerBuilder: A future-based function responsible for creating aMarkerwidget for both individual items and clusters. This is where you define the appearance and behavior of your markers.stopClusteringZoom: An optional parameter where clustering stops and all individual markers are shown beyond this zoom level.
_onMapCreated: Initializes the_mapControllerand passes it to the_manager. It then calls_manager.updateMap()to initially populate the map with markers._markerBuilder:- Checks if
cluster.isGeoclusterto determine if it's a cluster or a single marker. - If it's a cluster, tapping it animates the camera to zoom in, revealing the individual markers or smaller clusters.
- If it's an individual marker, it can show a SnackBar or navigate to a detail page.
_getClusterBitmap: A utility function to generate custom icons dynamically. For clusters, it draws a circle with the cluster count. For individual markers, it usesBitmapDescriptor.defaultMarkeror a custom image.
- Checks if
onCameraIdle: _manager.updateMap: Essential for triggering the cluster manager to re-evaluate and update the markers based on the new visible map region and zoom level after the camera stops moving.onCameraMove: _manager.onCameraMove: Notifies the cluster manager about camera movements, allowing it to perform calculations efficiently.
Conclusion
Integrating Google Maps with marker clustering in Flutter is crucial for building performant and user-friendly location-based applications that handle large datasets. By leveraging the google_maps_flutter and google_maps_cluster_manager packages, you can seamlessly implement this functionality with custom marker appearances and interactive behaviors. This setup provides a solid foundation, allowing you to focus on enriching your map experience with additional features and detailed information for your users.