image

13 Dec 2025

9K

35K

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 custom Place objects (which extend ClusterItem).
    • _updateMarkers: A callback function that the cluster manager invokes whenever the set of visible markers (individual or clustered) changes. You update your map's markers state here.
    • markerBuilder: A future-based function responsible for creating a Marker widget 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 _mapController and passes it to the _manager. It then calls _manager.updateMap() to initially populate the map with markers.
  • _markerBuilder:
    • Checks if cluster.isGeocluster to 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 uses BitmapDescriptor.defaultMarker or a custom image.
  • 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.

Related Articles

Dec 19, 2025

Flutter & Firebase Auth: Seamless Social Media Login

Flutter & Firebase Auth: Seamless Social Media Login In today's digital landscape, user authentication is a critical component of almost every application. Pro

Dec 19, 2025

Building a Widget List with Sticky

Building a Widget List with Sticky Header in Flutter Creating dynamic and engaging user interfaces is crucial for modern applications. One common UI pattern th

Dec 19, 2025

Mastering Transform Scale & Rotate Animations in Flutter

Mastering Transform Scale & Rotate Animations in Flutter Flutter's powerful animation framework allows developers to create visually stunning and highly intera