[google_maps_flutter_web] Initial support for custom overlays (#3538)

This is a resubmission of https://github.com/flutter/plugins/pull/6982 from the now archived flutter plugins repo. I'm submitting the changes from the original author, @AsturaPhoenix. The original description is below.

--------

Saves tile bytes to blobs and uses img elements to decode and render. Does not implement opacity, perform caching, or serve placeholder images.

**Issue:** Fixes https://github.com/flutter/flutter/issues/98596

**Known issues:**

- https://github.com/flutter/flutter/issues/116132
- https://github.com/AsturaPhoenix/trip_planner_aquamarine/issues/22
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 0427818..6a9b6ad 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.3
+
+* Initial support for custom overlays. [#98596](https://github.com/flutter/flutter/issues/98596).
+
 ## 0.5.2
 
 * Adds options for gesture handling and tilt controls.
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart
index 00a448a..fda38f2 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart
@@ -14,14 +14,14 @@
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
 
-import 'google_maps_controller_test.mocks.dart';
-
-@GenerateMocks(<Type>[], customMocks: <MockSpec<dynamic>>[
-  MockSpec<CirclesController>(onMissingStub: OnMissingStub.returnDefault),
-  MockSpec<PolygonsController>(onMissingStub: OnMissingStub.returnDefault),
-  MockSpec<PolylinesController>(onMissingStub: OnMissingStub.returnDefault),
-  MockSpec<MarkersController>(onMissingStub: OnMissingStub.returnDefault),
+@GenerateNiceMocks(<MockSpec<dynamic>>[
+  MockSpec<CirclesController>(),
+  MockSpec<PolygonsController>(),
+  MockSpec<PolylinesController>(),
+  MockSpec<MarkersController>(),
+  MockSpec<TileOverlaysController>(),
 ])
+import 'google_maps_controller_test.mocks.dart';
 
 /// Test Google Map Controller
 void main() {
@@ -194,6 +194,15 @@
           }, throwsAssertionError);
         });
 
+        testWidgets('cannot updateTileOverlays after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() {
+            controller.updateTileOverlays(const <TileOverlay>{});
+          }, throwsAssertionError);
+        });
+
         testWidgets('isInfoWindowShown defaults to false',
             (WidgetTester tester) async {
           controller.dispose();
@@ -208,6 +217,7 @@
       late MockMarkersController markers;
       late MockPolygonsController polygons;
       late MockPolylinesController polylines;
+      late MockTileOverlaysController tileOverlays;
       late gmaps.GMap map;
 
       setUp(() {
@@ -215,20 +225,20 @@
         markers = MockMarkersController();
         polygons = MockPolygonsController();
         polylines = MockPolylinesController();
+        tileOverlays = MockTileOverlaysController();
         map = gmaps.GMap(html.DivElement());
       });
 
       testWidgets('listens to map events', (WidgetTester tester) async {
-        controller = createController();
-        controller.debugSetOverrides(
-          createMap: (_, __) => map,
-          circles: circles,
-          markers: markers,
-          polygons: polygons,
-          polylines: polylines,
-        );
-
-        controller.init();
+        controller = createController()
+          ..debugSetOverrides(
+            createMap: (_, __) => map,
+            circles: circles,
+            markers: markers,
+            polygons: polygons,
+            polylines: polylines,
+          )
+          ..init();
 
         // Trigger events on the map, and verify they've been broadcast to the stream
         final Stream<MapEvent<Object?>> capturedEvents = stream.stream.take(5);
@@ -258,26 +268,26 @@
 
       testWidgets("binds geometry controllers to map's",
           (WidgetTester tester) async {
-        controller = createController();
-        controller.debugSetOverrides(
-          createMap: (_, __) => map,
-          circles: circles,
-          markers: markers,
-          polygons: polygons,
-          polylines: polylines,
-        );
-
-        controller.init();
+        controller = createController()
+          ..debugSetOverrides(
+            createMap: (_, __) => map,
+            circles: circles,
+            markers: markers,
+            polygons: polygons,
+            polylines: polylines,
+            tileOverlays: tileOverlays,
+          )
+          ..init();
 
         verify(circles.bindToMap(mapId, map));
         verify(markers.bindToMap(mapId, map));
         verify(polygons.bindToMap(mapId, map));
         verify(polylines.bindToMap(mapId, map));
+        verify(tileOverlays.bindToMap(mapId, map));
       });
 
       testWidgets('renders initial geometry', (WidgetTester tester) async {
-        controller = createController(
-            mapObjects: MapObjects(circles: <Circle>{
+        final MapObjects mapObjects = MapObjects(circles: <Circle>{
           const Circle(
             circleId: CircleId('circle-1'),
             zIndex: 1234,
@@ -320,57 +330,25 @@
             LatLng(43.354469, -5.851318),
             LatLng(43.354762, -5.850824),
           ])
-        }));
+        }, tileOverlays: <TileOverlay>{
+          const TileOverlay(tileOverlayId: TileOverlayId('overlay-1'))
+        });
 
-        controller.debugSetOverrides(
-          circles: circles,
-          markers: markers,
-          polygons: polygons,
-          polylines: polylines,
-        );
+        controller = createController(mapObjects: mapObjects)
+          ..debugSetOverrides(
+            circles: circles,
+            markers: markers,
+            polygons: polygons,
+            polylines: polylines,
+            tileOverlays: tileOverlays,
+          )
+          ..init();
 
-        controller.init();
-
-        final Set<Circle> capturedCircles =
-            verify(circles.addCircles(captureAny)).captured[0] as Set<Circle>;
-        final Set<Marker> capturedMarkers =
-            verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>;
-        final Set<Polygon> capturedPolygons =
-            verify(polygons.addPolygons(captureAny)).captured[0]
-                as Set<Polygon>;
-        final Set<Polyline> capturedPolylines =
-            verify(polylines.addPolylines(captureAny)).captured[0]
-                as Set<Polyline>;
-
-        expect(capturedCircles.first.circleId.value, 'circle-1');
-        expect(capturedCircles.first.zIndex, 1234);
-        expect(capturedMarkers.first.markerId.value, 'marker-1');
-        expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test');
-        expect(capturedMarkers.first.infoWindow.title, 'title for test');
-        expect(capturedPolygons.first.polygonId.value, 'polygon-1');
-        expect(capturedPolygons.elementAt(1).polygonId.value,
-            'polygon-2-with-holes');
-        expect(capturedPolygons.elementAt(1).holes, isNot(null));
-        expect(capturedPolylines.first.polylineId.value, 'polyline-1');
-      });
-
-      testWidgets('empty infoWindow does not create InfoWindow instance.',
-          (WidgetTester tester) async {
-        controller = createController(
-            mapObjects: MapObjects(markers: <Marker>{
-          const Marker(markerId: MarkerId('marker-1')),
-        }));
-
-        controller.debugSetOverrides(
-          markers: markers,
-        );
-
-        controller.init();
-
-        final Set<Marker> capturedMarkers =
-            verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>;
-
-        expect(capturedMarkers.first.infoWindow, InfoWindow.noText);
+        verify(circles.addCircles(mapObjects.circles));
+        verify(markers.addMarkers(mapObjects.markers));
+        verify(polygons.addPolygons(mapObjects.polygons));
+        verify(polylines.addPolylines(mapObjects.polylines));
+        verify(tileOverlays.addTileOverlays(mapObjects.tileOverlays));
       });
 
       group('Initialization options', () {
@@ -449,15 +427,12 @@
               target: LatLng(43.308, -5.6910),
               zoom: 12,
             ),
-          );
-
-          controller.debugSetOverrides(
-              createMap: (_, gmaps.MapOptions options) {
-            capturedOptions = options;
-            return map;
-          });
-
-          controller.init();
+          )
+            ..debugSetOverrides(createMap: (_, gmaps.MapOptions options) {
+              capturedOptions = options;
+              return map;
+            })
+            ..init();
 
           expect(capturedOptions, isNotNull);
           expect(capturedOptions!.zoom, 12);
@@ -467,8 +442,7 @@
 
       group('Traffic Layer', () {
         testWidgets('by default is disabled', (WidgetTester tester) async {
-          controller = createController();
-          controller.init();
+          controller = createController()..init();
           expect(controller.trafficLayer, isNull);
         });
 
@@ -477,9 +451,9 @@
           controller = createController(
               mapConfiguration: const MapConfiguration(
             trafficEnabled: true,
-          ));
-          controller.debugSetOverrides(createMap: (_, __) => map);
-          controller.init();
+          ))
+            ..debugSetOverrides(createMap: (_, __) => map)
+            ..init();
           expect(controller.trafficLayer, isNotNull);
         });
       });
@@ -496,9 +470,9 @@
             ..zoom = 10
             ..center = gmaps.LatLng(0, 0),
         );
-        controller = createController();
-        controller.debugSetOverrides(createMap: (_, __) => map);
-        controller.init();
+        controller = createController()
+          ..debugSetOverrides(createMap: (_, __) => map)
+          ..init();
       });
 
       group('updateRawOptions', () {
@@ -556,13 +530,9 @@
 
     // These are the methods that get forwarded to other controllers, so we just verify calls.
     group('Pass-through methods', () {
-      setUp(() {
-        controller = createController();
-      });
-
       testWidgets('updateCircles', (WidgetTester tester) async {
         final MockCirclesController mock = MockCirclesController();
-        controller.debugSetOverrides(circles: mock);
+        controller = createController()..debugSetOverrides(circles: mock);
 
         final Set<Circle> previous = <Circle>{
           const Circle(circleId: CircleId('to-be-updated')),
@@ -589,7 +559,7 @@
 
       testWidgets('updateMarkers', (WidgetTester tester) async {
         final MockMarkersController mock = MockMarkersController();
-        controller.debugSetOverrides(markers: mock);
+        controller = createController()..debugSetOverrides(markers: mock);
 
         final Set<Marker> previous = <Marker>{
           const Marker(markerId: MarkerId('to-be-updated')),
@@ -616,7 +586,7 @@
 
       testWidgets('updatePolygons', (WidgetTester tester) async {
         final MockPolygonsController mock = MockPolygonsController();
-        controller.debugSetOverrides(polygons: mock);
+        controller = createController()..debugSetOverrides(polygons: mock);
 
         final Set<Polygon> previous = <Polygon>{
           const Polygon(polygonId: PolygonId('to-be-updated')),
@@ -643,7 +613,7 @@
 
       testWidgets('updatePolylines', (WidgetTester tester) async {
         final MockPolylinesController mock = MockPolylinesController();
-        controller.debugSetOverrides(polylines: mock);
+        controller = createController()..debugSetOverrides(polylines: mock);
 
         final Set<Polyline> previous = <Polyline>{
           const Polyline(polylineId: PolylineId('to-be-updated')),
@@ -674,11 +644,38 @@
         }));
       });
 
+      testWidgets('updateTileOverlays', (WidgetTester tester) async {
+        final MockTileOverlaysController mock = MockTileOverlaysController();
+        controller = createController(
+            mapObjects: MapObjects(tileOverlays: <TileOverlay>{
+          const TileOverlay(tileOverlayId: TileOverlayId('to-be-updated')),
+          const TileOverlay(tileOverlayId: TileOverlayId('to-be-removed')),
+        }))
+          ..debugSetOverrides(tileOverlays: mock);
+
+        controller.updateTileOverlays(<TileOverlay>{
+          const TileOverlay(
+              tileOverlayId: TileOverlayId('to-be-updated'), visible: false),
+          const TileOverlay(tileOverlayId: TileOverlayId('to-be-added')),
+        });
+
+        verify(mock.removeTileOverlays(<TileOverlayId>{
+          const TileOverlayId('to-be-removed'),
+        }));
+        verify(mock.addTileOverlays(<TileOverlay>{
+          const TileOverlay(tileOverlayId: TileOverlayId('to-be-added')),
+        }));
+        verify(mock.changeTileOverlays(<TileOverlay>{
+          const TileOverlay(
+              tileOverlayId: TileOverlayId('to-be-updated'), visible: false),
+        }));
+      });
+
       testWidgets('infoWindow visibility', (WidgetTester tester) async {
         final MockMarkersController mock = MockMarkersController();
         const MarkerId markerId = MarkerId('marker-with-infowindow');
         when(mock.isInfoWindowShown(markerId)).thenReturn(true);
-        controller.debugSetOverrides(markers: mock);
+        controller = createController()..debugSetOverrides(markers: mock);
 
         controller.showInfoWindow(markerId);
 
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 4bc5f36..f73b3d7 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
@@ -403,3 +403,65 @@
         returnValueForMissingStub: null,
       );
 }
+
+/// A class which mocks [TileOverlaysController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTileOverlaysController extends _i1.Mock
+    implements _i3.TileOverlaysController {
+  @override
+  _i2.GMap get googleMap => (super.noSuchMethod(
+        Invocation.getter(#googleMap),
+        returnValue: _FakeGMap_0(
+          this,
+          Invocation.getter(#googleMap),
+        ),
+        returnValueForMissingStub: _FakeGMap_0(
+          this,
+          Invocation.getter(#googleMap),
+        ),
+      ) as _i2.GMap);
+  @override
+  set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
+        Invocation.setter(
+          #googleMap,
+          _googleMap,
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
+  void addTileOverlays(Set<_i4.TileOverlay>? tileOverlays) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #addTileOverlays,
+          [tileOverlays],
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
+  void changeTileOverlays(Set<_i4.TileOverlay>? tileOverlays) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #changeTileOverlays,
+          [tileOverlays],
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
+  void removeTileOverlays(Set<_i4.TileOverlayId>? tileOverlayIds) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #removeTileOverlays,
+          [tileOverlayIds],
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
+  void clearTileCache(_i4.TileOverlayId? tileOverlayId) => super.noSuchMethod(
+        Invocation.method(
+          #clearTileCache,
+          [tileOverlayId],
+        ),
+        returnValueForMissingStub: null,
+      );
+}
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 c773596..36b4d11 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
@@ -14,12 +14,9 @@
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
 
+@GenerateNiceMocks(<MockSpec<dynamic>>[MockSpec<GoogleMapController>()])
 import 'google_maps_plugin_test.mocks.dart';
 
-@GenerateMocks(<Type>[], customMocks: <MockSpec<dynamic>>[
-  MockSpec<GoogleMapController>(onMissingStub: OnMissingStub.returnDefault),
-])
-
 /// Test GoogleMapsPlugin
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -208,28 +205,6 @@
       });
     });
 
-    group('Noop methods:', () {
-      const int mapId = 0;
-      setUp(() {
-        plugin.debugSetMapById(<int, GoogleMapController>{mapId: controller});
-      });
-      // Options
-      testWidgets('updateTileOverlays', (WidgetTester tester) async {
-        final Future<void> update = plugin.updateTileOverlays(
-          mapId: mapId,
-          newTileOverlays: <TileOverlay>{},
-        );
-        expect(update, completion(null));
-      });
-      testWidgets('updateTileOverlays', (WidgetTester tester) async {
-        final Future<void> update = plugin.clearTileCache(
-          const TileOverlayId('any'),
-          mapId: mapId,
-        );
-        expect(update, completion(null));
-      });
-    });
-
     // These methods only pass-through values from the plugin to the controller
     // so we verify them all together here...
     group('Pass-through methods:', () {
@@ -287,6 +262,24 @@
 
         verify(controller.updateCircles(expectedUpdates));
       });
+      // Tile Overlays
+      testWidgets('updateTileOverlays', (WidgetTester tester) async {
+        final Set<TileOverlay> expectedOverlays = <TileOverlay>{
+          const TileOverlay(tileOverlayId: TileOverlayId('overlay'))
+        };
+
+        await plugin.updateTileOverlays(
+            newTileOverlays: expectedOverlays, mapId: mapId);
+
+        verify(controller.updateTileOverlays(expectedOverlays));
+      });
+      testWidgets('clearTileCache', (WidgetTester tester) async {
+        const TileOverlayId tileOverlayId = TileOverlayId('Dory');
+
+        await plugin.clearTileCache(tileOverlayId, mapId: mapId);
+
+        verify(controller.clearTileCache(tileOverlayId));
+      });
       // Camera
       testWidgets('animateCamera', (WidgetTester tester) async {
         final CameraUpdate expectedUpdates = CameraUpdate.newLatLng(
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 36e6052..831bda1 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
@@ -126,6 +126,7 @@
     _i4.CirclesController? circles,
     _i4.PolygonsController? polygons,
     _i4.PolylinesController? polylines,
+    _i4.TileOverlaysController? tileOverlays,
   }) =>
       super.noSuchMethod(
         Invocation.method(
@@ -137,6 +138,7 @@
             #circles: circles,
             #polygons: polygons,
             #polylines: polylines,
+            #tileOverlays: tileOverlays,
           },
         ),
         returnValueForMissingStub: null,
@@ -286,6 +288,23 @@
         returnValueForMissingStub: null,
       );
   @override
+  void updateTileOverlays(Set<_i2.TileOverlay>? newOverlays) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #updateTileOverlays,
+          [newOverlays],
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
+  void clearTileCache(_i2.TileOverlayId? id) => super.noSuchMethod(
+        Invocation.method(
+          #clearTileCache,
+          [id],
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
   void showInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod(
         Invocation.method(
           #showInfoWindow,
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlay_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlay_test.dart
new file mode 100644
index 0000000..29f902f
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlay_test.dart
@@ -0,0 +1,119 @@
+// 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:convert';
+import 'dart:html' as html;
+
+import 'package:flutter_test/flutter_test.dart';
+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:integration_test/integration_test.dart';
+
+import 'resources/tile16_base64.dart';
+
+class NoTileProvider implements TileProvider {
+  const NoTileProvider();
+
+  @override
+  Future<Tile> getTile(int x, int y, int? zoom) async => TileProvider.noTile;
+}
+
+class TestTileProvider implements TileProvider {
+  const TestTileProvider();
+
+  @override
+  Future<Tile> getTile(int x, int y, int? zoom) async =>
+      Tile(16, 16, const Base64Decoder().convert(tile16Base64));
+}
+
+/// Test Overlays
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('TileOverlayController', () {
+    const TileOverlayId id = TileOverlayId('');
+
+    testWidgets('minimal initialization', (WidgetTester tester) async {
+      final TileOverlayController controller = TileOverlayController(
+        tileOverlay: const TileOverlay(tileOverlayId: id),
+      );
+
+      final gmaps.Size size = controller.gmMapType.tileSize!;
+      expect(size.width, TileOverlayController.logicalTileSize);
+      expect(size.height, TileOverlayController.logicalTileSize);
+      expect(controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document),
+          null);
+    });
+
+    testWidgets('produces image tiles', (WidgetTester tester) async {
+      final TileOverlayController controller = TileOverlayController(
+        tileOverlay: const TileOverlay(
+          tileOverlayId: id,
+          tileProvider: TestTileProvider(),
+        ),
+      );
+
+      final html.ImageElement img =
+          controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document)!
+              as html.ImageElement;
+      expect(img.naturalWidth, 0);
+      expect(img.naturalHeight, 0);
+      expect(img.hidden, true);
+
+      // Wait until the image is fully loaded and decoded before re-reading its attributes.
+      await img.onLoad.first;
+      await img.decode();
+
+      expect(img.hidden, false);
+      expect(img.naturalWidth, 16);
+      expect(img.naturalHeight, 16);
+    });
+
+    testWidgets('update', (WidgetTester tester) async {
+      final TileOverlayController controller = TileOverlayController(
+        tileOverlay: const TileOverlay(
+          tileOverlayId: id,
+          tileProvider: NoTileProvider(),
+        ),
+      );
+      {
+        final html.ImageElement img =
+            controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document)!
+                as html.ImageElement;
+        await null; // let `getTile` `then` complete
+        expect(
+          img.src,
+          isEmpty,
+          reason: 'The NoTileProvider never updates the img src',
+        );
+      }
+
+      controller.update(const TileOverlay(
+        tileOverlayId: id,
+        tileProvider: TestTileProvider(),
+      ));
+      {
+        final html.ImageElement img =
+            controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document)!
+                as html.ImageElement;
+        await img.onLoad.first;
+        expect(
+          img.src,
+          isNotEmpty,
+          reason: 'The img `src` should eventually become the Blob URL.',
+        );
+      }
+
+      controller.update(const TileOverlay(tileOverlayId: id));
+      {
+        expect(
+          controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document),
+          null,
+          reason: 'Setting a null tileProvider should work.',
+        );
+      }
+    });
+  });
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.dart
new file mode 100644
index 0000000..8b6b346
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.dart
@@ -0,0 +1,172 @@
+// 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 'dart:html' as html;
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps/google_maps.dart' as gmaps;
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'
+    hide GoogleMapController;
+import 'package:integration_test/integration_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+@GenerateNiceMocks(<MockSpec<dynamic>>[MockSpec<TileProvider>()])
+import 'overlays_test.mocks.dart';
+
+MockTileProvider neverTileProvider() {
+  final MockTileProvider tileProvider = MockTileProvider();
+  when(tileProvider.getTile(any, any, any))
+      .thenAnswer((_) => Completer<Tile>().future);
+  return tileProvider;
+}
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('TileOverlaysController', () {
+    late TileOverlaysController controller;
+    late gmaps.GMap map;
+    late List<MockTileProvider> tileProviders;
+    late List<TileOverlay> tileOverlays;
+
+    /// Queries the current overlay map types for tiles at x = 0, y = 0, zoom =
+    /// 0.
+    void probeTiles() {
+      for (final gmaps.MapType? mapType in map.overlayMapTypes!.array!) {
+        mapType?.getTile!(gmaps.Point(0, 0), 0, html.document);
+      }
+    }
+
+    setUp(() {
+      controller = TileOverlaysController();
+      map = gmaps.GMap(html.DivElement());
+      controller.googleMap = map;
+
+      tileProviders = <MockTileProvider>[
+        for (int i = 0; i < 3; ++i) neverTileProvider()
+      ];
+
+      tileOverlays = <TileOverlay>[
+        for (int i = 0; i < 3; ++i)
+          TileOverlay(
+              tileOverlayId: TileOverlayId('$i'),
+              tileProvider: tileProviders[i],
+              zIndex: i)
+      ];
+    });
+
+    testWidgets('addTileOverlays', (WidgetTester tester) async {
+      controller.addTileOverlays(<TileOverlay>{...tileOverlays});
+      probeTiles();
+      verifyInOrder(<dynamic>[
+        tileProviders[0].getTile(any, any, any),
+        tileProviders[1].getTile(any, any, any),
+        tileProviders[2].getTile(any, any, any),
+      ]);
+      verifyNoMoreInteractions(tileProviders[0]);
+      verifyNoMoreInteractions(tileProviders[1]);
+      verifyNoMoreInteractions(tileProviders[2]);
+    });
+
+    testWidgets('changeTileOverlays', (WidgetTester tester) async {
+      controller.addTileOverlays(<TileOverlay>{...tileOverlays});
+
+      // Set overlay 0 visiblity to false; flip z ordering of 1 and 2, leaving 1
+      // unchanged.
+      controller.changeTileOverlays(<TileOverlay>{
+        tileOverlays[0].copyWith(visibleParam: false),
+        tileOverlays[2].copyWith(zIndexParam: 0),
+      });
+
+      probeTiles();
+
+      verifyInOrder(<dynamic>[
+        tileProviders[2].getTile(any, any, any),
+        tileProviders[1].getTile(any, any, any),
+      ]);
+      verifyZeroInteractions(tileProviders[0]);
+      verifyNoMoreInteractions(tileProviders[1]);
+      verifyNoMoreInteractions(tileProviders[2]);
+
+      // Re-enable overlay 0.
+      controller.changeTileOverlays(
+          <TileOverlay>{tileOverlays[0].copyWith(visibleParam: true)});
+
+      probeTiles();
+
+      verify(tileProviders[2].getTile(any, any, any));
+      verifyInOrder(<dynamic>[
+        tileProviders[0].getTile(any, any, any),
+        tileProviders[1].getTile(any, any, any),
+      ]);
+      verifyNoMoreInteractions(tileProviders[0]);
+      verifyNoMoreInteractions(tileProviders[1]);
+      verifyNoMoreInteractions(tileProviders[2]);
+    });
+
+    testWidgets(
+        'updating the z index of a hidden layer does not make it visible',
+        (WidgetTester tester) async {
+      controller.addTileOverlays(<TileOverlay>{...tileOverlays});
+
+      controller.changeTileOverlays(<TileOverlay>{
+        tileOverlays[0].copyWith(zIndexParam: -1, visibleParam: false),
+      });
+
+      probeTiles();
+      verifyZeroInteractions(tileProviders[0]);
+    });
+
+    testWidgets('removeTileOverlays', (WidgetTester tester) async {
+      controller.addTileOverlays(<TileOverlay>{...tileOverlays});
+
+      controller.removeTileOverlays(<TileOverlayId>{
+        tileOverlays[0].tileOverlayId,
+        tileOverlays[2].tileOverlayId,
+      });
+
+      probeTiles();
+
+      verify(tileProviders[1].getTile(any, any, any));
+      verifyZeroInteractions(tileProviders[0]);
+      verifyZeroInteractions(tileProviders[2]);
+    });
+
+    testWidgets('clearTileCache', (WidgetTester tester) async {
+      final Completer<GoogleMapController> controllerCompleter =
+          Completer<GoogleMapController>();
+      await tester.pumpWidget(MaterialApp(
+          home: Scaffold(
+              body: GoogleMap(
+        initialCameraPosition: const CameraPosition(
+          target: LatLng(43.3078, -5.6958),
+          zoom: 14,
+        ),
+        tileOverlays: <TileOverlay>{...tileOverlays.take(2)},
+        onMapCreated: (GoogleMapController value) {
+          controllerCompleter.complete(value);
+          addTearDown(() => value.dispose());
+        },
+      ))));
+
+      // This is needed to kick-off the rendering of the JS Map flutter widget
+      await tester.pump();
+      final GoogleMapController controller = await controllerCompleter.future;
+
+      await tester.pump();
+      verify(tileProviders[0].getTile(any, any, any));
+      verify(tileProviders[1].getTile(any, any, any));
+
+      await controller.clearTileCache(tileOverlays[0].tileOverlayId);
+
+      await tester.pump();
+      verify(tileProviders[0].getTile(any, any, any));
+      verifyNoMoreInteractions(tileProviders[1]);
+    });
+  });
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.mocks.dart
new file mode 100644
index 0000000..126f7c5
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/overlays_test.mocks.dart
@@ -0,0 +1,77 @@
+// Mocks generated by Mockito 5.4.1 from annotations
+// in google_maps_flutter_web_integration_tests/integration_test/overlays_test.dart.
+// Do not manually edit this file.
+
+// @dart=2.19
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i3;
+
+import 'package:google_maps_flutter_platform_interface/src/types/types.dart'
+    as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeTile_0 extends _i1.SmartFake implements _i2.Tile {
+  _FakeTile_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [TileProvider].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTileProvider extends _i1.Mock implements _i2.TileProvider {
+  @override
+  _i3.Future<_i2.Tile> getTile(
+    int? x,
+    int? y,
+    int? zoom,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #getTile,
+          [
+            x,
+            y,
+            zoom,
+          ],
+        ),
+        returnValue: _i3.Future<_i2.Tile>.value(_FakeTile_0(
+          this,
+          Invocation.method(
+            #getTile,
+            [
+              x,
+              y,
+              zoom,
+            ],
+          ),
+        )),
+        returnValueForMissingStub: _i3.Future<_i2.Tile>.value(_FakeTile_0(
+          this,
+          Invocation.method(
+            #getTile,
+            [
+              x,
+              y,
+              zoom,
+            ],
+          ),
+        )),
+      ) as _i3.Future<_i2.Tile>);
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/tile16_base64.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/tile16_base64.dart
new file mode 100644
index 0000000..0728b17
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/tile16_base64.dart
@@ -0,0 +1,9 @@
+// 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.
+
+/// 16x16 transparent png.
+const String tile16Base64 =
+    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAA'
+    'Cxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAATSURBVDhPYxgFo2AUjAIwYGAAAAQQAAGn'
+    'RHxjAAAAAElFTkSuQmCC';
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 df0b8de..65448ab 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
@@ -9,6 +9,7 @@
 import 'dart:html';
 import 'dart:js_util';
 
+import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -31,6 +32,8 @@
 part 'src/google_maps_flutter_web.dart';
 part 'src/marker.dart';
 part 'src/markers.dart';
+part 'src/overlay.dart';
+part 'src/overlays.dart';
 part 'src/polygon.dart';
 part 'src/polygons.dart';
 part 'src/polyline.dart';
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 f49a687..fbb1942 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
@@ -25,11 +25,13 @@
         _polygons = mapObjects.polygons,
         _polylines = mapObjects.polylines,
         _circles = mapObjects.circles,
+        _tileOverlays = mapObjects.tileOverlays,
         _lastMapConfiguration = mapConfiguration {
     _circlesController = CirclesController(stream: _streamController);
     _polygonsController = PolygonsController(stream: _streamController);
     _polylinesController = PolylinesController(stream: _streamController);
     _markersController = MarkersController(stream: _streamController);
+    _tileOverlaysController = TileOverlaysController();
 
     // Register the view factory that will hold the `_div` that holds the map in the DOM.
     // The `_div` needs to be created outside of the ViewFactory (and cached!) so we can
@@ -53,6 +55,7 @@
   final Set<Polygon> _polygons;
   final Set<Polyline> _polylines;
   final Set<Circle> _circles;
+  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();
@@ -108,6 +111,7 @@
   PolygonsController? _polygonsController;
   PolylinesController? _polylinesController;
   MarkersController? _markersController;
+  TileOverlaysController? _tileOverlaysController;
   // Keeps track if _attachGeometryControllers has been called or not.
   bool _controllersBoundToMap = false;
 
@@ -122,12 +126,14 @@
     CirclesController? circles,
     PolygonsController? polygons,
     PolylinesController? polylines,
+    TileOverlaysController? tileOverlays,
   }) {
     _overrideCreateMap = createMap;
     _markersController = markers ?? _markersController;
     _circlesController = circles ?? _circlesController;
     _polygonsController = polygons ?? _polygonsController;
     _polylinesController = polylines ?? _polylinesController;
+    _tileOverlaysController = tileOverlays ?? _tileOverlaysController;
   }
 
   DebugCreateMapFunction? _overrideCreateMap;
@@ -182,13 +188,7 @@
     _attachGeometryControllers(map);
 
     // Now attach the geometry, traffic and any other layers...
-    _renderInitialGeometry(
-      markers: _markers,
-      circles: _circles,
-      polygons: _polygons,
-      polylines: _polylines,
-    );
-
+    _renderInitialGeometry();
     _setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false);
   }
 
@@ -241,22 +241,20 @@
         'Cannot attach a map to a null PolylinesController instance.');
     assert(_markersController != null,
         'Cannot attach a map to a null MarkersController instance.');
+    assert(_tileOverlaysController != null,
+        'Cannot attach a map to a null TileOverlaysController instance.');
 
     _circlesController!.bindToMap(_mapId, map);
     _polygonsController!.bindToMap(_mapId, map);
     _polylinesController!.bindToMap(_mapId, map);
     _markersController!.bindToMap(_mapId, map);
+    _tileOverlaysController!.bindToMap(_mapId, map);
 
     _controllersBoundToMap = true;
   }
 
   // Renders the initial sets of geometry.
-  void _renderInitialGeometry({
-    Set<Marker> markers = const <Marker>{},
-    Set<Circle> circles = const <Circle>{},
-    Set<Polygon> polygons = const <Polygon>{},
-    Set<Polyline> polylines = const <Polyline>{},
-  }) {
+  void _renderInitialGeometry() {
     assert(
         _controllersBoundToMap,
         'Geometry controllers must be bound to a map before any geometry can '
@@ -266,10 +264,11 @@
     // in the [_attachGeometryControllers] method, which ensures that all these
     // controllers below are *not* null.
 
-    _markersController!.addMarkers(markers);
-    _circlesController!.addCircles(circles);
-    _polygonsController!.addPolygons(polygons);
-    _polylinesController!.addPolylines(polylines);
+    _markersController!.addMarkers(_markers);
+    _circlesController!.addCircles(_circles);
+    _polygonsController!.addPolygons(_polygons);
+    _polylinesController!.addPolylines(_polylines);
+    _tileOverlaysController!.addTileOverlays(_tileOverlays);
   }
 
   // Merges new options coming from the plugin into _lastConfiguration.
@@ -407,6 +406,25 @@
     _markersController?.removeMarkers(updates.markerIdsToRemove);
   }
 
+  /// Updates the set of [TileOverlay]s.
+  void updateTileOverlays(Set<TileOverlay> newOverlays) {
+    final MapsObjectUpdates<TileOverlay> updates =
+        MapsObjectUpdates<TileOverlay>.from(_tileOverlays, newOverlays,
+            objectName: 'tileOverlay');
+    assert(_tileOverlaysController != null,
+        'Cannot update tile overlays after dispose().');
+    _tileOverlaysController?.addTileOverlays(updates.objectsToAdd);
+    _tileOverlaysController?.changeTileOverlays(updates.objectsToChange);
+    _tileOverlaysController
+        ?.removeTileOverlays(updates.objectIdsToRemove.cast<TileOverlayId>());
+    _tileOverlays = newOverlays;
+  }
+
+  /// Clears the tile cache associated with the given [TileOverlayId].
+  void clearTileCache(TileOverlayId id) {
+    _tileOverlaysController?.clearTileCache(id);
+  }
+
   /// Shows the [InfoWindow] of the marker identified by its [MarkerId].
   void showInfoWindow(MarkerId markerId) {
     assert(_markersController != null,
@@ -439,6 +457,7 @@
     _polygonsController = null;
     _polylinesController = null;
     _markersController = 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 049a6a2..6b91e94 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
@@ -95,7 +95,7 @@
     required Set<TileOverlay> newTileOverlays,
     required int mapId,
   }) async {
-    return; // Noop for now!
+    _map(mapId).updateTileOverlays(newTileOverlays);
   }
 
   @override
@@ -103,7 +103,7 @@
     TileOverlayId tileOverlayId, {
     required int mapId,
   }) async {
-    return; // Noop for now!
+    _map(mapId).clearTileCache(tileOverlayId);
   }
 
   /// Applies the given `cameraUpdate` to the current viewport (with animation).
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlay.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlay.dart
new file mode 100644
index 0000000..86f5387
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlay.dart
@@ -0,0 +1,76 @@
+// 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.
+
+part of google_maps_flutter_web;
+
+/// This wraps a [TileOverlay] in a [gmaps.MapType].
+class TileOverlayController {
+  /// Creates a `TileOverlayController` that wraps a [TileOverlay] object and its corresponding [gmaps.MapType].
+  TileOverlayController({
+    required TileOverlay tileOverlay,
+  }) {
+    update(tileOverlay);
+  }
+
+  /// The size in pixels of the (square) tiles passed to the Maps SDK.
+  ///
+  /// Even though the web supports any size, and rectangular tiles, for
+  /// for consistency with mobile, this is not configurable on the web.
+  /// (Both Android and iOS prefer square 256px tiles @ 1x DPI)
+  ///
+  /// For higher DPI screens, the Tile that is actually returned can be larger
+  /// than 256px square.
+  static const int logicalTileSize = 256;
+
+  /// Updates the [gmaps.MapType] and cached properties with an updated
+  /// [TileOverlay].
+  void update(TileOverlay tileOverlay) {
+    _tileOverlay = tileOverlay;
+    _gmMapType = gmaps.MapType()
+      ..tileSize = gmaps.Size(logicalTileSize, logicalTileSize)
+      ..getTile = _getTile;
+  }
+
+  /// Renders a Tile for gmaps; delegating to the configured [TileProvider].
+  HtmlElement? _getTile(
+    gmaps.Point? tileCoord,
+    num? zoom,
+    Document? ownerDocument,
+  ) {
+    if (_tileOverlay.tileProvider == null) {
+      return null;
+    }
+
+    final ImageElement img =
+        ownerDocument!.createElement('img') as ImageElement;
+    img.width = img.height = logicalTileSize;
+    img.hidden = true;
+    img.setAttribute('decoding', 'async');
+
+    _tileOverlay.tileProvider!
+        .getTile(tileCoord!.x!.toInt(), tileCoord.y!.toInt(), zoom?.toInt())
+        .then((Tile tile) {
+      if (tile.data == null) {
+        return;
+      }
+      // Using img lets us take advantage of native decoding.
+      final String src = Url.createObjectUrl(Blob(<Object?>[tile.data]));
+      img.src = src;
+      img.addEventListener('load', (_) {
+        img.hidden = false;
+        Url.revokeObjectUrl(src);
+      });
+    });
+
+    return img;
+  }
+
+  /// The [gmaps.MapType] produced by this controller.
+  gmaps.MapType get gmMapType => _gmMapType;
+  late gmaps.MapType _gmMapType;
+
+  /// The [TileOverlay] providing data for this controller.
+  TileOverlay get tileOverlay => _tileOverlay;
+  late TileOverlay _tileOverlay;
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlays.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlays.dart
new file mode 100644
index 0000000..aa6c191
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/overlays.dart
@@ -0,0 +1,99 @@
+// 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.
+
+part of google_maps_flutter_web;
+
+/// This class manages all the [TileOverlayController]s associated to a [GoogleMapController].
+class TileOverlaysController extends GeometryController {
+  final Map<TileOverlayId, TileOverlayController> _tileOverlays =
+      <TileOverlayId, TileOverlayController>{};
+  final List<TileOverlayController> _visibleTileOverlays =
+      <TileOverlayController>[];
+
+  // Inserts `tileOverlayController` into the list of visible overlays, and the current [googleMap].
+  //
+  // After insertion, the arrays stay sorted by ascending z-index.
+  void _insertZSorted(TileOverlayController tileOverlayController) {
+    final int index = _visibleTileOverlays.lowerBoundBy<num>(
+        tileOverlayController,
+        (TileOverlayController c) => c.tileOverlay.zIndex);
+
+    googleMap.overlayMapTypes!.insertAt(index, tileOverlayController.gmMapType);
+    _visibleTileOverlays.insert(index, tileOverlayController);
+  }
+
+  // Removes `tileOverlayController` from the list of visible overlays.
+  void _remove(TileOverlayController tileOverlayController) {
+    final int index = _visibleTileOverlays.indexOf(tileOverlayController);
+    if (index < 0) {
+      return;
+    }
+
+    googleMap.overlayMapTypes!.removeAt(index);
+    _visibleTileOverlays.removeAt(index);
+  }
+
+  /// Adds new [TileOverlay]s to this controller.
+  ///
+  /// Wraps the [TileOverlay]s in corresponding [TileOverlayController]s.
+  void addTileOverlays(Set<TileOverlay> tileOverlaysToAdd) {
+    tileOverlaysToAdd.forEach(_addTileOverlay);
+  }
+
+  void _addTileOverlay(TileOverlay tileOverlay) {
+    final TileOverlayController controller = TileOverlayController(
+      tileOverlay: tileOverlay,
+    );
+    _tileOverlays[tileOverlay.tileOverlayId] = controller;
+
+    if (tileOverlay.visible) {
+      _insertZSorted(controller);
+    }
+  }
+
+  /// Updates [TileOverlay]s with new options.
+  void changeTileOverlays(Set<TileOverlay> tileOverlays) {
+    tileOverlays.forEach(_changeTileOverlay);
+  }
+
+  void _changeTileOverlay(TileOverlay tileOverlay) {
+    final TileOverlayController controller =
+        _tileOverlays[tileOverlay.tileOverlayId]!;
+
+    final bool wasVisible = controller.tileOverlay.visible;
+    final bool isVisible = tileOverlay.visible;
+
+    controller.update(tileOverlay);
+
+    if (wasVisible) {
+      _remove(controller);
+    }
+    if (isVisible) {
+      _insertZSorted(controller);
+    }
+  }
+
+  /// Removes the tile overlays associated with the given [TileOverlayId]s.
+  void removeTileOverlays(Set<TileOverlayId> tileOverlayIds) {
+    tileOverlayIds.forEach(_removeTileOverlay);
+  }
+
+  void _removeTileOverlay(TileOverlayId tileOverlayId) {
+    final TileOverlayController? controller =
+        _tileOverlays.remove(tileOverlayId);
+    if (controller != null) {
+      _remove(controller);
+    }
+  }
+
+  /// Invalidates the tile overlay associated with the given [TileOverlayId].
+  void clearTileCache(TileOverlayId tileOverlayId) {
+    final TileOverlayController? controller = _tileOverlays[tileOverlayId];
+    if (controller != null && controller.tileOverlay.visible) {
+      final int i = _visibleTileOverlays.indexOf(controller);
+      // This causes the map to reload the overlay.
+      googleMap.overlayMapTypes!.setAt(i, controller.gmMapType);
+    }
+  }
+}
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 f1eb09c..642c2e4 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.2
+version: 0.5.3
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -17,6 +17,7 @@
         fileName: google_maps_flutter_web.dart
 
 dependencies:
+  collection: ^1.16.0
   flutter:
     sdk: flutter
   flutter_web_plugins: