[google_maps_flutter_web] Add marker clustering support (#6187)

This PR introduces support for marker clustering for Web platform

This is prequel PR for: https://github.com/flutter/packages/pull/4319
and sequel PR for: https://github.com/flutter/packages/pull/6158

Containing only changes to `google_maps_flutter_web` package.

Follow up PR will hold the app-facing plugin implementation.

Linked issue: https://github.com/flutter/flutter/issues/26863

---------

Co-authored-by: David Iglesias Teixeira <ditman@gmail.com>
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS
index b5b0e84..906ab21 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS
+++ b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS
@@ -65,3 +65,4 @@
 Alex Li <google@alexv525.com>
 Rahul Raj <64.rahulraj@gmail.com>
 Justin Baumann <me@jxstxn.dev>
+Joonas Kerttula <joonas.kerttula@codemate.com>
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md
index 08d4608..77f222b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.5.7
+
+* Adds support for marker clustering.
+
 ## 0.5.6+2
 
 * Uses `TrustedTypes` from `web: ^0.5.1`.
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md
index b52da95..22acc45 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/README.md
+++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md
@@ -50,6 +50,19 @@
 
 Now you should be able to use the Google Maps plugin normally.
 
+## Marker clustering
+
+If you need marker clustering support, modify the <head> tag to load the [js-markerclusterer](https://github.com/googlemaps/js-markerclusterer#install) library. Ensure you are using the currently supported version `2.5.3`, like so:
+
+```html
+<head>
+
+  <!-- // Other stuff -->
+
+  <script src="https://unpkg.com/@googlemaps/markerclusterer@2.5.3/dist/index.min.js"></script>
+</head>
+```
+
 ## Limitations of the web version
 
 The following map options are not available in web, because the map doesn't rotate there:
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart
index c855999..ee6ffe0 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart
@@ -42,7 +42,6 @@
         returnValue: <_i4.CircleId, _i3.CircleController>{},
         returnValueForMissingStub: <_i4.CircleId, _i3.CircleController>{},
       ) as Map<_i4.CircleId, _i3.CircleController>);
-
   @override
   _i2.GMap get googleMap => (super.noSuchMethod(
         Invocation.getter(#googleMap),
@@ -55,7 +54,6 @@
           Invocation.getter(#googleMap),
         ),
       ) as _i2.GMap);
-
   @override
   set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
         Invocation.setter(
@@ -64,14 +62,12 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   int get mapId => (super.noSuchMethod(
         Invocation.getter(#mapId),
         returnValue: 0,
         returnValueForMissingStub: 0,
       ) as int);
-
   @override
   set mapId(int? _mapId) => super.noSuchMethod(
         Invocation.setter(
@@ -80,7 +76,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void addCircles(Set<_i4.Circle>? circlesToAdd) => super.noSuchMethod(
         Invocation.method(
@@ -89,7 +84,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void changeCircles(Set<_i4.Circle>? circlesToChange) => super.noSuchMethod(
         Invocation.method(
@@ -98,7 +92,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void removeCircles(Set<_i4.CircleId>? circleIdsToRemove) =>
       super.noSuchMethod(
@@ -108,7 +101,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void bindToMap(
     int? mapId,
@@ -137,7 +129,6 @@
         returnValue: <_i4.PolygonId, _i3.PolygonController>{},
         returnValueForMissingStub: <_i4.PolygonId, _i3.PolygonController>{},
       ) as Map<_i4.PolygonId, _i3.PolygonController>);
-
   @override
   _i2.GMap get googleMap => (super.noSuchMethod(
         Invocation.getter(#googleMap),
@@ -150,7 +141,6 @@
           Invocation.getter(#googleMap),
         ),
       ) as _i2.GMap);
-
   @override
   set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
         Invocation.setter(
@@ -159,14 +149,12 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   int get mapId => (super.noSuchMethod(
         Invocation.getter(#mapId),
         returnValue: 0,
         returnValueForMissingStub: 0,
       ) as int);
-
   @override
   set mapId(int? _mapId) => super.noSuchMethod(
         Invocation.setter(
@@ -175,7 +163,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => super.noSuchMethod(
         Invocation.method(
@@ -184,7 +171,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void changePolygons(Set<_i4.Polygon>? polygonsToChange) => super.noSuchMethod(
         Invocation.method(
@@ -193,7 +179,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) =>
       super.noSuchMethod(
@@ -203,7 +188,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void bindToMap(
     int? mapId,
@@ -232,7 +216,6 @@
         returnValue: <_i4.PolylineId, _i3.PolylineController>{},
         returnValueForMissingStub: <_i4.PolylineId, _i3.PolylineController>{},
       ) as Map<_i4.PolylineId, _i3.PolylineController>);
-
   @override
   _i2.GMap get googleMap => (super.noSuchMethod(
         Invocation.getter(#googleMap),
@@ -245,7 +228,6 @@
           Invocation.getter(#googleMap),
         ),
       ) as _i2.GMap);
-
   @override
   set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
         Invocation.setter(
@@ -254,14 +236,12 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   int get mapId => (super.noSuchMethod(
         Invocation.getter(#mapId),
         returnValue: 0,
         returnValueForMissingStub: 0,
       ) as int);
-
   @override
   set mapId(int? _mapId) => super.noSuchMethod(
         Invocation.setter(
@@ -270,7 +250,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => super.noSuchMethod(
         Invocation.method(
@@ -279,7 +258,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void changePolylines(Set<_i4.Polyline>? polylinesToChange) =>
       super.noSuchMethod(
@@ -289,7 +267,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) =>
       super.noSuchMethod(
@@ -299,7 +276,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void bindToMap(
     int? mapId,
@@ -327,7 +303,6 @@
         returnValue: <_i4.MarkerId, _i3.MarkerController>{},
         returnValueForMissingStub: <_i4.MarkerId, _i3.MarkerController>{},
       ) as Map<_i4.MarkerId, _i3.MarkerController>);
-
   @override
   _i2.GMap get googleMap => (super.noSuchMethod(
         Invocation.getter(#googleMap),
@@ -340,7 +315,6 @@
           Invocation.getter(#googleMap),
         ),
       ) as _i2.GMap);
-
   @override
   set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
         Invocation.setter(
@@ -349,14 +323,12 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   int get mapId => (super.noSuchMethod(
         Invocation.getter(#mapId),
         returnValue: 0,
         returnValueForMissingStub: 0,
       ) as int);
-
   @override
   set mapId(int? _mapId) => super.noSuchMethod(
         Invocation.setter(
@@ -365,7 +337,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void addMarkers(Set<_i4.Marker>? markersToAdd) => super.noSuchMethod(
         Invocation.method(
@@ -374,7 +345,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void changeMarkers(Set<_i4.Marker>? markersToChange) => super.noSuchMethod(
         Invocation.method(
@@ -383,7 +353,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) =>
       super.noSuchMethod(
@@ -393,7 +362,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void showMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(
         Invocation.method(
@@ -402,7 +370,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void hideMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(
         Invocation.method(
@@ -411,7 +378,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   bool isInfoWindowShown(_i4.MarkerId? markerId) => (super.noSuchMethod(
         Invocation.method(
@@ -421,7 +387,6 @@
         returnValue: false,
         returnValueForMissingStub: false,
       ) as bool);
-
   @override
   void bindToMap(
     int? mapId,
@@ -456,7 +421,6 @@
           Invocation.getter(#googleMap),
         ),
       ) as _i2.GMap);
-
   @override
   set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
         Invocation.setter(
@@ -465,14 +429,12 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   int get mapId => (super.noSuchMethod(
         Invocation.getter(#mapId),
         returnValue: 0,
         returnValueForMissingStub: 0,
       ) as int);
-
   @override
   set mapId(int? _mapId) => super.noSuchMethod(
         Invocation.setter(
@@ -481,7 +443,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void addTileOverlays(Set<_i4.TileOverlay>? tileOverlaysToAdd) =>
       super.noSuchMethod(
@@ -491,7 +452,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void changeTileOverlays(Set<_i4.TileOverlay>? tileOverlays) =>
       super.noSuchMethod(
@@ -501,7 +461,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void removeTileOverlays(Set<_i4.TileOverlayId>? tileOverlayIds) =>
       super.noSuchMethod(
@@ -511,7 +470,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void clearTileCache(_i4.TileOverlayId? tileOverlayId) => super.noSuchMethod(
         Invocation.method(
@@ -520,7 +478,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void bindToMap(
     int? mapId,
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart
index 36b4d11..b84b267 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart
@@ -361,10 +361,10 @@
       });
 
       testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async {
-        when(controller.isInfoWindowShown(any)).thenReturn(true);
-
         const MarkerId markerId = MarkerId('testing-123');
 
+        when(controller.isInfoWindowShown(markerId)).thenReturn(true);
+
         await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId);
 
         verify(controller.isInfoWindowShown(markerId));
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart
index 3f84b40..582d6dd 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart
@@ -9,6 +9,7 @@
 import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'
     as _i2;
 import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4;
+import 'package:google_maps_flutter_web/src/marker_clustering.dart' as _i6;
 import 'package:mockito/mockito.dart' as _i1;
 
 // ignore_for_file: type=lint
@@ -94,42 +95,37 @@
           Invocation.getter(#configuration),
         ),
       ) as _i2.MapConfiguration);
-
   @override
-  _i3.StreamController<_i2.MapEvent<Object?>> get stream => (super.noSuchMethod(
+  _i3.StreamController<_i2.MapEvent<dynamic>> get stream => (super.noSuchMethod(
         Invocation.getter(#stream),
-        returnValue: _FakeStreamController_1<_i2.MapEvent<Object?>>(
+        returnValue: _FakeStreamController_1<_i2.MapEvent<dynamic>>(
           this,
           Invocation.getter(#stream),
         ),
         returnValueForMissingStub:
-            _FakeStreamController_1<_i2.MapEvent<Object?>>(
+            _FakeStreamController_1<_i2.MapEvent<dynamic>>(
           this,
           Invocation.getter(#stream),
         ),
-      ) as _i3.StreamController<_i2.MapEvent<Object?>>);
-
+      ) as _i3.StreamController<_i2.MapEvent<dynamic>>);
   @override
-  _i3.Stream<_i2.MapEvent<Object?>> get events => (super.noSuchMethod(
+  _i3.Stream<_i2.MapEvent<dynamic>> get events => (super.noSuchMethod(
         Invocation.getter(#events),
-        returnValue: _i3.Stream<_i2.MapEvent<Object?>>.empty(),
-        returnValueForMissingStub: _i3.Stream<_i2.MapEvent<Object?>>.empty(),
-      ) as _i3.Stream<_i2.MapEvent<Object?>>);
-
+        returnValue: _i3.Stream<_i2.MapEvent<dynamic>>.empty(),
+        returnValueForMissingStub: _i3.Stream<_i2.MapEvent<dynamic>>.empty(),
+      ) as _i3.Stream<_i2.MapEvent<dynamic>>);
   @override
   bool get isInitialized => (super.noSuchMethod(
         Invocation.getter(#isInitialized),
         returnValue: false,
         returnValueForMissingStub: false,
       ) as bool);
-
   @override
   List<_i5.MapTypeStyle> get styles => (super.noSuchMethod(
         Invocation.getter(#styles),
         returnValue: <_i5.MapTypeStyle>[],
         returnValueForMissingStub: <_i5.MapTypeStyle>[],
       ) as List<_i5.MapTypeStyle>);
-
   @override
   void debugSetOverrides({
     _i4.DebugCreateMapFunction? createMap,
@@ -138,6 +134,7 @@
     _i4.CirclesController? circles,
     _i4.PolygonsController? polygons,
     _i4.PolylinesController? polylines,
+    _i6.ClusterManagersController? clusterManagers,
     _i4.TileOverlaysController? tileOverlays,
   }) =>
       super.noSuchMethod(
@@ -151,12 +148,12 @@
             #circles: circles,
             #polygons: polygons,
             #polylines: polylines,
+            #clusterManagers: clusterManagers,
             #tileOverlays: tileOverlays,
           },
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void init() => super.noSuchMethod(
         Invocation.method(
@@ -165,7 +162,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void updateMapConfiguration(_i2.MapConfiguration? update) =>
       super.noSuchMethod(
@@ -175,7 +171,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void updateStyles(List<_i5.MapTypeStyle>? styles) => super.noSuchMethod(
         Invocation.method(
@@ -184,7 +179,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   _i3.Future<_i2.LatLngBounds> getVisibleRegion() => (super.noSuchMethod(
         Invocation.method(
@@ -207,7 +201,6 @@
           ),
         )),
       ) as _i3.Future<_i2.LatLngBounds>);
-
   @override
   _i3.Future<_i2.ScreenCoordinate> getScreenCoordinate(_i2.LatLng? latLng) =>
       (super.noSuchMethod(
@@ -232,7 +225,6 @@
           ),
         )),
       ) as _i3.Future<_i2.ScreenCoordinate>);
-
   @override
   _i3.Future<_i2.LatLng> getLatLng(_i2.ScreenCoordinate? screenCoordinate) =>
       (super.noSuchMethod(
@@ -255,7 +247,6 @@
           ),
         )),
       ) as _i3.Future<_i2.LatLng>);
-
   @override
   _i3.Future<void> moveCamera(_i2.CameraUpdate? cameraUpdate) =>
       (super.noSuchMethod(
@@ -266,7 +257,6 @@
         returnValue: _i3.Future<void>.value(),
         returnValueForMissingStub: _i3.Future<void>.value(),
       ) as _i3.Future<void>);
-
   @override
   _i3.Future<double> getZoomLevel() => (super.noSuchMethod(
         Invocation.method(
@@ -276,7 +266,6 @@
         returnValue: _i3.Future<double>.value(0.0),
         returnValueForMissingStub: _i3.Future<double>.value(0.0),
       ) as _i3.Future<double>);
-
   @override
   void updateCircles(_i2.CircleUpdates? updates) => super.noSuchMethod(
         Invocation.method(
@@ -285,7 +274,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void updatePolygons(_i2.PolygonUpdates? updates) => super.noSuchMethod(
         Invocation.method(
@@ -294,7 +282,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void updatePolylines(_i2.PolylineUpdates? updates) => super.noSuchMethod(
         Invocation.method(
@@ -303,7 +290,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void updateMarkers(_i2.MarkerUpdates? updates) => super.noSuchMethod(
         Invocation.method(
@@ -312,7 +298,15 @@
         ),
         returnValueForMissingStub: null,
       );
-
+  @override
+  void updateClusterManagers(_i2.ClusterManagerUpdates? updates) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #updateClusterManagers,
+          [updates],
+        ),
+        returnValueForMissingStub: null,
+      );
   @override
   void updateTileOverlays(Set<_i2.TileOverlay>? newOverlays) =>
       super.noSuchMethod(
@@ -322,7 +316,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void clearTileCache(_i2.TileOverlayId? id) => super.noSuchMethod(
         Invocation.method(
@@ -331,7 +324,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void showInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod(
         Invocation.method(
@@ -340,7 +332,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   void hideInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod(
         Invocation.method(
@@ -349,7 +340,6 @@
         ),
         returnValueForMissingStub: null,
       );
-
   @override
   bool isInfoWindowShown(_i2.MarkerId? markerId) => (super.noSuchMethod(
         Invocation.method(
@@ -359,7 +349,6 @@
         returnValue: false,
         returnValueForMissingStub: false,
       ) as bool);
-
   @override
   void dispose() => super.noSuchMethod(
         Invocation.method(
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart
new file mode 100644
index 0000000..c34d9c5
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart
@@ -0,0 +1,170 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: unnecessary_nullable_for_final_variable_declarations
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+import 'package:integration_test/integration_test.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  GoogleMapsFlutterPlatform.instance.enableDebugInspection();
+  final GoogleMapsFlutterPlatform plugin = GoogleMapsFlutterPlatform.instance;
+  final GoogleMapsInspectorPlatform inspector =
+      GoogleMapsInspectorPlatform.instance!;
+
+  const LatLng mapCenter = LatLng(20, 20);
+  const CameraPosition initialCameraPosition =
+      CameraPosition(target: mapCenter);
+
+  group('MarkersController', () {
+    const int testMapId = 33930;
+
+    testWidgets('Marker clustering', (WidgetTester tester) async {
+      const ClusterManagerId clusterManagerId = ClusterManagerId('cluster 1');
+
+      final Set<ClusterManager> clusterManagers = <ClusterManager>{
+        const ClusterManager(clusterManagerId: clusterManagerId),
+      };
+
+      // Create the marker with clusterManagerId.
+      final Set<Marker> initialMarkers = <Marker>{
+        const Marker(
+            markerId: MarkerId('1'),
+            position: mapCenter,
+            clusterManagerId: clusterManagerId),
+        const Marker(
+            markerId: MarkerId('2'),
+            position: mapCenter,
+            clusterManagerId: clusterManagerId),
+      };
+
+      final Completer<int> mapIdCompleter = Completer<int>();
+
+      await _pumpMap(
+          tester,
+          plugin.buildViewWithConfiguration(
+              testMapId, (int id) => mapIdCompleter.complete(id),
+              widgetConfiguration: const MapWidgetConfiguration(
+                initialCameraPosition: initialCameraPosition,
+                textDirection: TextDirection.ltr,
+              ),
+              mapObjects: MapObjects(
+                  clusterManagers: clusterManagers, markers: initialMarkers)));
+
+      final int mapId = await mapIdCompleter.future;
+      expect(mapId, equals(testMapId));
+
+      addTearDown(() => plugin.dispose(mapId: mapId));
+
+      final LatLng latlon = await plugin
+          .getLatLng(const ScreenCoordinate(x: 0, y: 0), mapId: mapId);
+      debugPrint(latlon.toString());
+
+      final List<Cluster> clusters =
+          await waitForValueMatchingPredicate<List<Cluster>>(
+                  tester,
+                  () async => inspector.getClusters(
+                      mapId: mapId, clusterManagerId: clusterManagerId),
+                  (List<Cluster> clusters) => clusters.isNotEmpty) ??
+              <Cluster>[];
+
+      expect(clusters.length, 1);
+      expect(clusters[0].markerIds.length, 2);
+
+      // Copy only the first marker with null clusterManagerId.
+      // This means that both markers should be removed from the cluster.
+      final Set<Marker> updatedMarkers = <Marker>{
+        _copyMarkerWithClusterManagerId(initialMarkers.first, null)
+      };
+
+      final MarkerUpdates markerUpdates =
+          MarkerUpdates.from(initialMarkers, updatedMarkers);
+      await plugin.updateMarkers(markerUpdates, mapId: mapId);
+
+      final List<Cluster> updatedClusters =
+          await waitForValueMatchingPredicate<List<Cluster>>(
+                  tester,
+                  () async => inspector.getClusters(
+                      mapId: mapId, clusterManagerId: clusterManagerId),
+                  (List<Cluster> clusters) => clusters.isNotEmpty) ??
+              <Cluster>[];
+
+      expect(updatedClusters.length, 0);
+    });
+  });
+}
+
+// Repeatedly checks an asynchronous value against a test condition, waiting
+// one frame between each check, returing the value if it passes the predicate
+// before [maxTries] is reached.
+//
+// Returns null if the predicate is never satisfied.
+//
+// This is useful for cases where the Maps SDK has some internally
+// asynchronous operation that we don't have visibility into (e.g., native UI
+// animations).
+Future<T?> waitForValueMatchingPredicate<T>(WidgetTester tester,
+    Future<T> Function() getValue, bool Function(T) predicate,
+    {int maxTries = 100}) async {
+  for (int i = 0; i < maxTries; i++) {
+    final T value = await getValue();
+    if (predicate(value)) {
+      return value;
+    }
+    await tester.pump();
+  }
+  return null;
+}
+
+Marker _copyMarkerWithClusterManagerId(
+    Marker marker, ClusterManagerId? clusterManagerId) {
+  return Marker(
+    markerId: marker.markerId,
+    alpha: marker.alpha,
+    anchor: marker.anchor,
+    consumeTapEvents: marker.consumeTapEvents,
+    draggable: marker.draggable,
+    flat: marker.flat,
+    icon: marker.icon,
+    infoWindow: marker.infoWindow,
+    position: marker.position,
+    rotation: marker.rotation,
+    visible: marker.visible,
+    zIndex: marker.zIndex,
+    onTap: marker.onTap,
+    onDragStart: marker.onDragStart,
+    onDrag: marker.onDrag,
+    onDragEnd: marker.onDragEnd,
+    clusterManagerId: clusterManagerId,
+  );
+}
+
+/// Pumps a [map] widget in [tester] of a certain [size], then waits until it settles.
+Future<void> _pumpMap(WidgetTester tester, Widget map,
+    [Size size = const Size.square(200)]) async {
+  await tester.pumpWidget(_wrapMap(map, size));
+  await tester.pumpAndSettle();
+}
+
+/// Wraps a [map] in a bunch of widgets so it renders in all platforms.
+///
+/// An optional [size] can be passed.
+Widget _wrapMap(Widget map, [Size size = const Size.square(200)]) {
+  return MaterialApp(
+    home: Scaffold(
+      body: Center(
+        child: SizedBox.fromSize(
+          size: size,
+          child: map,
+        ),
+      ),
+    ),
+  );
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart
index d965435..81e1eb2 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart
@@ -11,6 +11,7 @@
 import 'package:google_maps/google_maps.dart' as gmaps;
 import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
 import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
+import 'package:google_maps_flutter_web/src/marker_clustering.dart';
 // ignore: implementation_imports
 import 'package:google_maps_flutter_web/src/utils.dart';
 import 'package:http/http.dart' as http;
@@ -25,12 +26,17 @@
   group('MarkersController', () {
     late StreamController<MapEvent<Object?>> events;
     late MarkersController controller;
+    late ClusterManagersController clusterManagersController;
     late gmaps.GMap map;
 
     setUp(() {
       events = StreamController<MapEvent<Object?>>();
-      controller = MarkersController(stream: events);
+
+      clusterManagersController = ClusterManagersController(stream: events);
+      controller = MarkersController(
+          stream: events, clusterManagersController: clusterManagersController);
       map = gmaps.GMap(createDivElement());
+      clusterManagersController.bindToMap(123, map);
       controller.bindToMap(123, map);
     });
 
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml
index 2838157..a7222df 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml
@@ -9,7 +9,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  google_maps_flutter_platform_interface: ^2.5.0
+  google_maps_flutter_platform_interface: ^2.6.0
   google_maps_flutter_web:
     path: ../
   web: ^0.5.0
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html
index 3121d18..9cbd7be 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html
@@ -7,6 +7,7 @@
         <title>Browser Tests</title>
         <!-- This API key comes from: go/flutter-maps-web-tests-api-key (GCP project: flutter-infra) -->
         <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAa9cRBkhuxGq3Xw3HPz8SPwaVOhRmm7kk&libraries=geometry"></script>
+        <script src="https://unpkg.com/@googlemaps/markerclusterer@2.5.3/dist/index.min.js"></script>
     </head>
     <body>
         <script src="main.dart.js"></script>
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart
index fe44d41..cda20cf 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart
@@ -24,6 +24,7 @@
 import 'src/dom_window_extension.dart';
 import 'src/google_maps_inspector_web.dart';
 import 'src/map_styler.dart';
+import 'src/marker_clustering.dart';
 import 'src/third_party/to_screen_location/to_screen_location.dart';
 import 'src/types.dart';
 import 'src/utils.dart';
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart
index 0f73a7d..1822a2f 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart
@@ -172,20 +172,22 @@
   return gmaps.LatLng(latLng.latitude, latLng.longitude);
 }
 
-LatLng _gmLatLngToLatLng(gmaps.LatLng latLng) {
+/// Converts [gmaps.LatLng] to [LatLng].
+LatLng gmLatLngToLatLng(gmaps.LatLng latLng) {
   return LatLng(latLng.lat.toDouble(), latLng.lng.toDouble());
 }
 
-LatLngBounds _gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) {
+/// Converts a [gmaps.LatLngBounds] into a [LatLngBounds].
+LatLngBounds gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) {
   return LatLngBounds(
-    southwest: _gmLatLngToLatLng(latLngBounds.southWest),
-    northeast: _gmLatLngToLatLng(latLngBounds.northEast),
+    southwest: gmLatLngToLatLng(latLngBounds.southWest),
+    northeast: gmLatLngToLatLng(latLngBounds.northEast),
   );
 }
 
 CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) {
   return CameraPosition(
-    target: _gmLatLngToLatLng(map.center ?? _nullGmapsLatLng),
+    target: gmLatLngToLatLng(map.center ?? _nullGmapsLatLng),
     bearing: map.heading?.toDouble() ?? 0,
     tilt: map.tilt?.toDouble() ?? 0,
     zoom: map.zoom?.toDouble() ?? 0,
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart
index c60dd92..c2fe750 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart
@@ -29,12 +29,17 @@
         _polygons = mapObjects.polygons,
         _polylines = mapObjects.polylines,
         _circles = mapObjects.circles,
+        _clusterManagers = mapObjects.clusterManagers,
         _tileOverlays = mapObjects.tileOverlays,
         _lastMapConfiguration = mapConfiguration {
     _circlesController = CirclesController(stream: _streamController);
     _polygonsController = PolygonsController(stream: _streamController);
     _polylinesController = PolylinesController(stream: _streamController);
-    _markersController = MarkersController(stream: _streamController);
+    _clusterManagersController =
+        ClusterManagersController(stream: _streamController);
+    _markersController = MarkersController(
+        stream: _streamController,
+        clusterManagersController: _clusterManagersController!);
     _tileOverlaysController = TileOverlaysController();
     _updateStylesFromConfiguration(mapConfiguration);
 
@@ -60,7 +65,9 @@
   final Set<Polygon> _polygons;
   final Set<Polyline> _polylines;
   final Set<Circle> _circles;
+  final Set<ClusterManager> _clusterManagers;
   Set<TileOverlay> _tileOverlays;
+
   // The configuration passed by the user, before converting to gmaps.
   // Caching this allows us to re-create the map faithfully when needed.
   MapConfiguration _lastMapConfiguration = const MapConfiguration();
@@ -118,13 +125,20 @@
   PolygonsController? _polygonsController;
   PolylinesController? _polylinesController;
   MarkersController? _markersController;
+  ClusterManagersController? _clusterManagersController;
   TileOverlaysController? _tileOverlaysController;
+
   // Keeps track if _attachGeometryControllers has been called or not.
   bool _controllersBoundToMap = false;
 
   // Keeps track if the map is moving or not.
   bool _mapIsMoving = false;
 
+  /// The ClusterManagersController of this Map. Only for integration testing.
+  @visibleForTesting
+  ClusterManagersController? get clusterManagersController =>
+      _clusterManagersController;
+
   /// Overrides certain properties to install mocks defined during testing.
   @visibleForTesting
   void debugSetOverrides({
@@ -134,6 +148,7 @@
     CirclesController? circles,
     PolygonsController? polygons,
     PolylinesController? polylines,
+    ClusterManagersController? clusterManagers,
     TileOverlaysController? tileOverlays,
   }) {
     _overrideCreateMap = createMap;
@@ -142,6 +157,7 @@
     _circlesController = circles ?? _circlesController;
     _polygonsController = polygons ?? _polygonsController;
     _polylinesController = polylines ?? _polylinesController;
+    _clusterManagersController = clusterManagers ?? _clusterManagersController;
     _tileOverlaysController = tileOverlays ?? _tileOverlaysController;
   }
 
@@ -197,6 +213,8 @@
     _attachMapEvents(map);
     _attachGeometryControllers(map);
 
+    _initClustering(_clusterManagers);
+
     // Now attach the geometry, traffic and any other layers...
     _renderInitialGeometry();
     _setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false);
@@ -211,13 +229,13 @@
     map.onClick.listen((gmaps.IconMouseEvent event) {
       assert(event.latLng != null);
       _streamController.add(
-        MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
+        MapTapEvent(_mapId, gmLatLngToLatLng(event.latLng!)),
       );
     });
     map.onRightclick.listen((gmaps.MapMouseEvent event) {
       assert(event.latLng != null);
       _streamController.add(
-        MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
+        MapLongPressEvent(_mapId, gmLatLngToLatLng(event.latLng!)),
       );
     });
     map.onBoundsChanged.listen((void _) {
@@ -251,6 +269,8 @@
         'Cannot attach a map to a null PolylinesController instance.');
     assert(_markersController != null,
         'Cannot attach a map to a null MarkersController instance.');
+    assert(_clusterManagersController != null,
+        'Cannot attach a map to a null ClusterManagersController instance.');
     assert(_tileOverlaysController != null,
         'Cannot attach a map to a null TileOverlaysController instance.');
 
@@ -258,11 +278,16 @@
     _polygonsController!.bindToMap(_mapId, map);
     _polylinesController!.bindToMap(_mapId, map);
     _markersController!.bindToMap(_mapId, map);
+    _clusterManagersController!.bindToMap(_mapId, map);
     _tileOverlaysController!.bindToMap(_mapId, map);
 
     _controllersBoundToMap = true;
   }
 
+  void _initClustering(Set<ClusterManager> clusterManagers) {
+    _clusterManagersController!.addClusterManagers(clusterManagers);
+  }
+
   // Renders the initial sets of geometry.
   void _renderInitialGeometry() {
     assert(
@@ -369,7 +394,7 @@
         await Future<gmaps.LatLngBounds?>.value(_googleMap!.bounds) ??
             _nullGmapsLatLngBounds;
 
-    return _gmLatLngBoundsTolatLngBounds(bounds);
+    return gmLatLngBoundsTolatLngBounds(bounds);
   }
 
   /// Returns the [ScreenCoordinate] for a given viewport [LatLng].
@@ -390,7 +415,7 @@
 
     final gmaps.LatLng latLng =
         _pixelToLatLng(_googleMap!, screenCoordinate.x, screenCoordinate.y);
-    return _gmLatLngToLatLng(latLng);
+    return gmLatLngToLatLng(latLng);
   }
 
   /// Applies a `cameraUpdate` to the current viewport.
@@ -447,6 +472,16 @@
     _markersController?.removeMarkers(updates.markerIdsToRemove);
   }
 
+  /// Applies [ClusterManagerUpdates] to the currently managed cluster managers.
+  void updateClusterManagers(ClusterManagerUpdates updates) {
+    assert(_clusterManagersController != null,
+        'Cannot update markers after dispose().');
+    _clusterManagersController
+        ?.addClusterManagers(updates.clusterManagersToAdd);
+    _clusterManagersController
+        ?.removeClusterManagers(updates.clusterManagerIdsToRemove);
+  }
+
   /// Updates the set of [TileOverlay]s.
   void updateTileOverlays(Set<TileOverlay> newOverlays) {
     final MapsObjectUpdates<TileOverlay> updates =
@@ -498,6 +533,7 @@
     _polygonsController = null;
     _polylinesController = null;
     _markersController = null;
+    _clusterManagersController = null;
     _tileOverlaysController = null;
     _streamController.close();
   }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart
index 805887f..f71818a 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart
@@ -99,6 +99,14 @@
   }
 
   @override
+  Future<void> updateClusterManagers(
+    ClusterManagerUpdates clusterManagerUpdates, {
+    required int mapId,
+  }) async {
+    _map(mapId).updateClusterManagers(clusterManagerUpdates);
+  }
+
+  @override
   Future<void> clearTileCache(
     TileOverlayId tileOverlayId, {
     required int mapId,
@@ -280,6 +288,11 @@
   }
 
   @override
+  Stream<ClusterTapEvent> onClusterTap({required int mapId}) {
+    return _events(mapId).whereType<ClusterTapEvent>();
+  }
+
+  @override
   Future<String?> getStyleError({required int mapId}) async {
     return _map(mapId).lastStyleError;
   }
@@ -339,6 +352,7 @@
   void enableDebugInspection() {
     GoogleMapsInspectorPlatform.instance = GoogleMapsInspectorWeb(
       (int mapId) => _map(mapId).configuration,
+      (int mapId) => _map(mapId).clusterManagersController,
     );
   }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart
index 6d95531..98b4743 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart
@@ -3,17 +3,25 @@
 // found in the LICENSE file.
 
 import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+import 'marker_clustering.dart';
 
 /// Function that gets the [MapConfiguration] for a given `mapId`.
 typedef ConfigurationProvider = MapConfiguration Function(int mapId);
 
+/// Function that gets the [ClusterManagersController] for a given `mapId`.
+typedef ClusterManagersControllerProvider = ClusterManagersController? Function(
+    int mapId);
+
 /// This platform implementation allows inspecting the running maps.
 class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform {
   /// Build an "inspector" that is able to look into maps.
-  GoogleMapsInspectorWeb(ConfigurationProvider configurationProvider)
-      : _configurationProvider = configurationProvider;
+  GoogleMapsInspectorWeb(ConfigurationProvider configurationProvider,
+      ClusterManagersControllerProvider clusterManagersControllerProvider)
+      : _configurationProvider = configurationProvider,
+        _clusterManagersControllerProvider = clusterManagersControllerProvider;
 
   final ConfigurationProvider _configurationProvider;
+  final ClusterManagersControllerProvider _clusterManagersControllerProvider;
 
   @override
   Future<bool> areBuildingsEnabled({required int mapId}) async {
@@ -85,4 +93,14 @@
   Future<bool> isTrafficEnabled({required int mapId}) async {
     return _configurationProvider(mapId).trafficEnabled ?? false;
   }
+
+  @override
+  Future<List<Cluster>> getClusters({
+    required int mapId,
+    required ClusterManagerId clusterManagerId,
+  }) async {
+    return _clusterManagersControllerProvider(mapId)
+            ?.getClusters(clusterManagerId) ??
+        <Cluster>[];
+  }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart
index c1b0772..518dce6 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart
@@ -15,9 +15,11 @@
     LatLngCallback? onDrag,
     LatLngCallback? onDragEnd,
     VoidCallback? onTap,
+    ClusterManagerId? clusterManagerId,
   })  : _marker = marker,
         _infoWindow = infoWindow,
-        _consumeTapEvents = consumeTapEvents {
+        _consumeTapEvents = consumeTapEvents,
+        _clusterManagerId = clusterManagerId {
     if (onTap != null) {
       marker.onClick.listen((gmaps.MapMouseEvent event) {
         onTap.call();
@@ -47,6 +49,8 @@
 
   final bool _consumeTapEvents;
 
+  final ClusterManagerId? _clusterManagerId;
+
   final gmaps.InfoWindow? _infoWindow;
 
   bool _infoWindowShown = false;
@@ -57,6 +61,9 @@
   /// Returns `true` if the [gmaps.InfoWindow] associated to this marker is being shown.
   bool get infoWindowShown => _infoWindowShown;
 
+  /// Returns [ClusterManagerId] if marker belongs to cluster.
+  ClusterManagerId? get clusterManagerId => _clusterManagerId;
+
   /// Returns the [gmaps.Marker] associated to this controller.
   gmaps.Marker? get marker => _marker;
 
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart
new file mode 100644
index 0000000..d4b5ed3
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart
@@ -0,0 +1,139 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:google_maps/google_maps.dart' as gmaps;
+import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+
+import '../google_maps_flutter_web.dart';
+import 'marker_clustering_js_interop.dart';
+import 'types.dart';
+
+/// A controller class for managing marker clustering.
+///
+/// This class maps [ClusterManager] objects to javascript [MarkerClusterer]
+/// objects and provides an interface for adding and removing markers from
+/// clusters.
+class ClusterManagersController extends GeometryController {
+  /// Creates a new [ClusterManagersController] instance.
+  ///
+  /// The [stream] parameter is a required [StreamController] used for
+  /// emitting map events.
+  ClusterManagersController(
+      {required StreamController<MapEvent<Object?>> stream})
+      : _streamController = stream,
+        _clusterManagerIdToMarkerClusterer =
+            <ClusterManagerId, MarkerClusterer>{};
+
+  // The stream over which cluster managers broadcast their events
+  final StreamController<MapEvent<Object?>> _streamController;
+
+  // A cache of [MarkerClusterer]s indexed by their [ClusterManagerId].
+  final Map<ClusterManagerId, MarkerClusterer>
+      _clusterManagerIdToMarkerClusterer;
+
+  /// Adds a set of [ClusterManager] objects to the cache.
+  void addClusterManagers(Set<ClusterManager> clusterManagersToAdd) {
+    clusterManagersToAdd.forEach(_addClusterManager);
+  }
+
+  void _addClusterManager(ClusterManager clusterManager) {
+    final MarkerClusterer markerClusterer = createMarkerClusterer(
+        googleMap,
+        (gmaps.MapMouseEvent event, MarkerClustererCluster cluster,
+                gmaps.GMap map) =>
+            _clusterClicked(
+                clusterManager.clusterManagerId, event, cluster, map));
+
+    _clusterManagerIdToMarkerClusterer[clusterManager.clusterManagerId] =
+        markerClusterer;
+    markerClusterer.onAdd();
+  }
+
+  /// Removes a set of [ClusterManagerId]s from the cache.
+  void removeClusterManagers(Set<ClusterManagerId> clusterManagerIdsToRemove) {
+    clusterManagerIdsToRemove.forEach(_removeClusterManager);
+  }
+
+  void _removeClusterManager(ClusterManagerId clusterManagerId) {
+    final MarkerClusterer? markerClusterer =
+        _clusterManagerIdToMarkerClusterer[clusterManagerId];
+    if (markerClusterer != null) {
+      markerClusterer.clearMarkers(true);
+      markerClusterer.onRemove();
+    }
+    _clusterManagerIdToMarkerClusterer.remove(clusterManagerId);
+  }
+
+  /// Adds given [gmaps.Marker] to the [MarkerClusterer] with given
+  /// [ClusterManagerId].
+  void addItem(ClusterManagerId clusterManagerId, gmaps.Marker marker) {
+    final MarkerClusterer? markerClusterer =
+        _clusterManagerIdToMarkerClusterer[clusterManagerId];
+    if (markerClusterer != null) {
+      markerClusterer.addMarker(marker, true);
+      markerClusterer.render();
+    }
+  }
+
+  /// Removes given [gmaps.Marker] from the [MarkerClusterer] with given
+  /// [ClusterManagerId].
+  void removeItem(ClusterManagerId clusterManagerId, gmaps.Marker? marker) {
+    if (marker != null) {
+      final MarkerClusterer? markerClusterer =
+          _clusterManagerIdToMarkerClusterer[clusterManagerId];
+      if (markerClusterer != null) {
+        markerClusterer.removeMarker(marker, true);
+        markerClusterer.render();
+      }
+    }
+  }
+
+  /// Returns list of clusters in [MarkerClusterer] with given
+  /// [ClusterManagerId].
+  List<Cluster> getClusters(ClusterManagerId clusterManagerId) {
+    final MarkerClusterer? markerClusterer =
+        _clusterManagerIdToMarkerClusterer[clusterManagerId];
+    if (markerClusterer != null) {
+      return markerClusterer.clusters
+          .map((MarkerClustererCluster cluster) =>
+              _convertCluster(clusterManagerId, cluster))
+          .toList();
+    }
+    return <Cluster>[];
+  }
+
+  void _clusterClicked(
+      ClusterManagerId clusterManagerId,
+      gmaps.MapMouseEvent event,
+      MarkerClustererCluster markerClustererCluster,
+      gmaps.GMap map) {
+    if (markerClustererCluster.count > 0 &&
+        markerClustererCluster.bounds != null) {
+      final Cluster cluster =
+          _convertCluster(clusterManagerId, markerClustererCluster);
+      _streamController.add(ClusterTapEvent(mapId, cluster));
+    }
+  }
+
+  /// Converts [MarkerClustererCluster] to [Cluster].
+  Cluster _convertCluster(ClusterManagerId clusterManagerId,
+      MarkerClustererCluster markerClustererCluster) {
+    final LatLng position = gmLatLngToLatLng(markerClustererCluster.position);
+    final LatLngBounds bounds =
+        gmLatLngBoundsTolatLngBounds(markerClustererCluster.bounds!);
+
+    final List<MarkerId> markerIds = markerClustererCluster.markers
+        .map<MarkerId>((gmaps.Marker marker) =>
+            MarkerId(marker.get('markerId')! as String))
+        .toList();
+    return Cluster(
+      clusterManagerId,
+      markerIds,
+      position: position,
+      bounds: bounds,
+    );
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart
new file mode 100644
index 0000000..e3bf42b
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart
@@ -0,0 +1,164 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TODO(srujzs): Needed for https://github.com/dart-lang/sdk/issues/54801. Once
+// we publish a version with a min SDK constraint that contains this fix,
+// remove.
+@JS()
+library;
+
+import 'dart:js_interop';
+
+import 'package:google_maps/google_maps.dart' as gmaps;
+
+/// A typedef representing a callback function for handling cluster tap events.
+typedef ClusterClickHandler = void Function(
+  gmaps.MapMouseEvent,
+  MarkerClustererCluster,
+  gmaps.GMap,
+);
+
+/// The [MarkerClustererOptions] object used to initialize [MarkerClusterer].
+///
+/// See: https://googlemaps.github.io/js-markerclusterer/interfaces/MarkerClustererOptions.html
+@JS()
+@anonymous
+extension type MarkerClustererOptions._(JSObject _) implements JSObject {
+  /// Constructs a new [MarkerClustererOptions] object.
+  factory MarkerClustererOptions({
+    gmaps.GMap? map,
+    List<gmaps.Marker>? markers,
+    ClusterClickHandler? onClusterClick,
+  }) =>
+      MarkerClustererOptions._js(
+        map: map as JSAny?,
+        markers: markers?.cast<JSAny>().toJS ?? JSArray<JSAny>(),
+        onClusterClick: onClusterClick != null
+            ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) =>
+                onClusterClick(event as gmaps.MapMouseEvent, cluster,
+                    map as gmaps.GMap)).toJS
+            : null,
+      );
+
+  external factory MarkerClustererOptions._js({
+    JSAny? map,
+    JSArray<JSAny> markers,
+    JSFunction? onClusterClick,
+  });
+
+  /// Returns the [gmaps.GMap] object.
+  gmaps.GMap? get map => _map as gmaps.GMap?;
+  @JS('map')
+  external JSAny? get _map;
+
+  /// Returns the list of [gmaps.Marker] objects.
+  List<gmaps.Marker>? get markers => _markers?.toDart.cast<gmaps.Marker>();
+  @JS('markers')
+  external JSArray<JSAny>? get _markers;
+
+  /// Returns the onClusterClick handler.
+  ClusterClickHandler? get onClusterClick =>
+      _onClusterClick?.toDart as ClusterClickHandler?;
+  @JS('onClusterClick')
+  external JSExportedDartFunction? get _onClusterClick;
+}
+
+/// The cluster object handled by the [MarkerClusterer].
+///
+/// https://googlemaps.github.io/js-markerclusterer/classes/Cluster.html
+@JS('markerClusterer.Cluster')
+extension type MarkerClustererCluster._(JSObject _) implements JSObject {
+  /// Getter for the cluster marker.
+  gmaps.Marker get marker => _marker as gmaps.Marker;
+  @JS('marker')
+  external JSAny get _marker;
+
+  /// List of markers in the cluster.
+  List<gmaps.Marker> get markers => _markers.toDart.cast<gmaps.Marker>();
+  @JS('markers')
+  external JSArray<JSAny> get _markers;
+
+  /// The bounds of the cluster.
+  gmaps.LatLngBounds? get bounds => _bounds as gmaps.LatLngBounds?;
+  @JS('bounds')
+  external JSAny? get _bounds;
+
+  /// The position of the cluster marker.
+  gmaps.LatLng get position => _position as gmaps.LatLng;
+  @JS('position')
+  external JSAny get _position;
+
+  /// Get the count of **visible** markers.
+  external int get count;
+
+  /// Deletes the cluster.
+  external void delete();
+
+  /// Adds a marker to the cluster.
+  void push(gmaps.Marker marker) => _push(marker as JSAny);
+  @JS('push')
+  external void _push(JSAny marker);
+}
+
+/// The [MarkerClusterer] object used to cluster markers on the map.
+///
+/// https://googlemaps.github.io/js-markerclusterer/classes/MarkerClusterer.html
+@JS('markerClusterer.MarkerClusterer')
+extension type MarkerClusterer._(JSObject _) implements JSObject {
+  /// Constructs a new [MarkerClusterer] object.
+  external MarkerClusterer(MarkerClustererOptions options);
+
+  /// Adds a marker to be clustered by the [MarkerClusterer].
+  void addMarker(gmaps.Marker marker, bool? noDraw) =>
+      _addMarker(marker as JSAny, noDraw);
+  @JS('addMarker')
+  external void _addMarker(JSAny marker, bool? noDraw);
+
+  /// Adds a list of markers to be clustered by the [MarkerClusterer].
+  void addMarkers(List<gmaps.Marker>? markers, bool? noDraw) =>
+      _addMarkers(markers?.cast<JSAny>().toJS, noDraw);
+  @JS('addMarkers')
+  external void _addMarkers(JSArray<JSAny>? markers, bool? noDraw);
+
+  /// Removes a marker from the [MarkerClusterer].
+  bool removeMarker(gmaps.Marker marker, bool? noDraw) =>
+      _removeMarker(marker as JSAny, noDraw);
+  @JS('removeMarker')
+  external bool _removeMarker(JSAny marker, bool? noDraw);
+
+  /// Removes a list of markers from the [MarkerClusterer].
+  bool removeMarkers(List<gmaps.Marker>? markers, bool? noDraw) =>
+      _removeMarkers(markers?.cast<JSAny>().toJS, noDraw);
+  @JS('removeMarkers')
+  external bool _removeMarkers(JSArray<JSAny>? markers, bool? noDraw);
+
+  /// Clears all the markers from the [MarkerClusterer].
+  external void clearMarkers(bool? noDraw);
+
+  /// Called when the [MarkerClusterer] is added to the map.
+  external void onAdd();
+
+  /// Called when the [MarkerClusterer] is removed from the map.
+  external void onRemove();
+
+  /// Returns the list of clusters.
+  List<MarkerClustererCluster> get clusters =>
+      _clusters.toDart.cast<MarkerClustererCluster>();
+  @JS('clusters')
+  external JSArray<JSAny> get _clusters;
+
+  /// Recalculates and draws all the marker clusters.
+  external void render();
+}
+
+/// Creates [MarkerClusterer] object with given [gmaps.GMap] and
+/// [ClusterClickHandler].
+MarkerClusterer createMarkerClusterer(
+    gmaps.GMap map, ClusterClickHandler onClusterClickHandler) {
+  final MarkerClustererOptions options = MarkerClustererOptions(
+    map: map,
+    onClusterClick: onClusterClickHandler,
+  );
+  return MarkerClusterer(options);
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart
index 26cb94f..962422f 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart
@@ -9,7 +9,9 @@
   /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers.
   MarkersController({
     required StreamController<MapEvent<Object?>> stream,
+    required ClusterManagersController clusterManagersController,
   })  : _streamController = stream,
+        _clusterManagersController = clusterManagersController,
         _markerIdToController = <MarkerId, MarkerController>{};
 
   // A cache of [MarkerController]s indexed by their [MarkerId].
@@ -18,6 +20,8 @@
   // The stream over which markers broadcast their events
   final StreamController<MapEvent<Object?>> _streamController;
 
+  final ClusterManagersController _clusterManagersController;
+
   /// Returns the cache of [MarkerController]s. Test only.
   @visibleForTesting
   Map<MarkerId, MarkerController> get markers => _markerIdToController;
@@ -53,9 +57,19 @@
 
     final gmaps.MarkerOptions markerOptions =
         _markerOptionsFromMarker(marker, currentMarker);
-    final gmaps.Marker gmMarker = gmaps.Marker(markerOptions)..map = googleMap;
+
+    final gmaps.Marker gmMarker = gmaps.Marker(markerOptions);
+
+    gmMarker.set('markerId', marker.markerId.value);
+
+    if (marker.clusterManagerId != null) {
+      _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker);
+    } else {
+      gmMarker.map = googleMap;
+    }
     final MarkerController controller = MarkerController(
       marker: gmMarker,
+      clusterManagerId: marker.clusterManagerId,
       infoWindow: gmInfoWindow,
       consumeTapEvents: marker.consumeTapEvents,
       onTap: () {
@@ -84,16 +98,26 @@
     final MarkerController? markerController =
         _markerIdToController[marker.markerId];
     if (markerController != null) {
-      final gmaps.MarkerOptions markerOptions = _markerOptionsFromMarker(
-        marker,
-        markerController.marker,
-      );
-      final gmaps.InfoWindowOptions? infoWindow =
-          _infoWindowOptionsFromMarker(marker);
-      markerController.update(
-        markerOptions,
-        newInfoWindowContent: infoWindow?.content as HTMLElement?,
-      );
+      final ClusterManagerId? oldClusterManagerId =
+          markerController.clusterManagerId;
+      final ClusterManagerId? newClusterManagerId = marker.clusterManagerId;
+
+      if (oldClusterManagerId != newClusterManagerId) {
+        // If clusterManagerId changes. Remove existing marker and create new one.
+        _removeMarker(marker.markerId);
+        _addMarker(marker);
+      } else {
+        final gmaps.MarkerOptions markerOptions = _markerOptionsFromMarker(
+          marker,
+          markerController.marker,
+        );
+        final gmaps.InfoWindowOptions? infoWindow =
+            _infoWindowOptionsFromMarker(marker);
+        markerController.update(
+          markerOptions,
+          newInfoWindowContent: infoWindow?.content as HTMLElement?,
+        );
+      }
     }
   }
 
@@ -104,6 +128,10 @@
 
   void _removeMarker(MarkerId markerId) {
     final MarkerController? markerController = _markerIdToController[markerId];
+    if (markerController?.clusterManagerId != null) {
+      _clusterManagersController.removeItem(
+          markerController!.clusterManagerId!, markerController.marker);
+    }
     markerController?.remove();
     _markerIdToController.remove(markerId);
   }
@@ -151,7 +179,7 @@
   void _onMarkerDragStart(MarkerId markerId, gmaps.LatLng latLng) {
     _streamController.add(MarkerDragStartEvent(
       mapId,
-      _gmLatLngToLatLng(latLng),
+      gmLatLngToLatLng(latLng),
       markerId,
     ));
   }
@@ -159,7 +187,7 @@
   void _onMarkerDrag(MarkerId markerId, gmaps.LatLng latLng) {
     _streamController.add(MarkerDragEvent(
       mapId,
-      _gmLatLngToLatLng(latLng),
+      gmLatLngToLatLng(latLng),
       markerId,
     ));
   }
@@ -167,7 +195,7 @@
   void _onMarkerDragEnd(MarkerId markerId, gmaps.LatLng latLng) {
     _streamController.add(MarkerDragEndEvent(
       mapId,
-      _gmLatLngToLatLng(latLng),
+      gmLatLngToLatLng(latLng),
       markerId,
     ));
   }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
index f5c1793..3447e8b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of google_maps_flutter
 repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
-version: 0.5.6+2
+version: 0.5.7
 
 environment:
   sdk: ^3.3.0
@@ -23,7 +23,7 @@
   flutter_web_plugins:
     sdk: flutter
   google_maps: ^7.1.0
-  google_maps_flutter_platform_interface: ^2.5.0
+  google_maps_flutter_platform_interface: ^2.6.0
   sanitize_html: ^2.0.0
   stream_transform: ^2.0.0
   web: ^0.5.1