[google_maps_flutter_web] Migrate to null-safety. (#3726)

* Updates JS-interop package to google_maps ^5.1.0
* Breaking changes:
  * The property `icon` of a `Marker` cannot be `null`. Defaults to `BitmapDescriptor.defaultMarker`
  * The property `initialCameraPosition` of a `GoogleMapController` can't be `null`. It is also marked as `required`.
  * The parameter `creationId` of the `buildView` method cannot be `null` (this should be handled internally for users of the plugin)
  * Most of the Controller methods can't be called after `remove`/`dispose`. Calling these methods now will throw an Assertion error. Before it'd be a no-op, or a null-pointer exception.
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 29e9287..fd56b64 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,12 @@
+## 0.3.0
+
+* Migrate package to null-safety.
+* **Breaking changes:**
+  * The property `icon` of a `Marker` cannot be `null`. Defaults to `BitmapDescriptor.defaultMarker`
+  * The property `initialCameraPosition` of a `GoogleMapController` can't be `null`. It is also marked as `required`.
+  * The parameter `creationId` of the `buildView` method cannot be `null` (this should be handled internally for users of the plugin)
+  * Most of the Controller methods can't be called after `remove`/`dispose`. Calling these methods now will throw an Assertion error. Before it'd be a no-op, or a null-pointer exception.
+
 ## 0.2.1
 
 * Move integration tests to `example`.
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/README.md b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md
index 0ec01e0..582288a 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/README.md
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md
@@ -19,3 +19,13 @@
 
     * Single: `./run_test.sh integration_test/TEST_NAME.dart`
     * All: `./run_test.sh`
+
+## Mocks
+
+There's new `.mocks.dart` files next to the test files that use them.
+
+Mock files are [generated by `package:mockito`](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#code-generation). The contents of these files can change with how the mocks are used within the tests, in addition to actual changes in the APIs they're mocking.
+
+Mock files can be updated either manually by running the following command: `flutter pub run build_runner build` (or the `regen_mocks.sh` script), or automatically on each call to the `run_test.sh` script.
+
+Please, add whatever changes show up in mock files to your PRs, or CI will fail.
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/build.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/build.yaml
new file mode 100644
index 0000000..db3104b
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/build.yaml
@@ -0,0 +1,6 @@
+targets:
+  $default:
+    sources:
+      - integration_test/*.dart
+      - lib/$lib$
+      - $package$
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 fd4df2e..1d33eea 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
@@ -2,43 +2,30 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.9
-
 import 'dart:async';
+import 'dart:html' as html;
 
-import 'package:integration_test/integration_test.dart';
-import 'package:google_maps/google_maps.dart' as gmaps;
-import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
+import 'package:flutter/widgets.dart';
 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 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
 
-import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+import 'google_maps_controller_test.mocks.dart';
 
-class _MockCirclesController extends Mock implements CirclesController {}
+// This value is used when comparing long~num, like
+// LatLng values.
+const _acceptableDelta = 0.0000000001;
 
-class _MockPolygonsController extends Mock implements PolygonsController {}
-
-class _MockPolylinesController extends Mock implements PolylinesController {}
-
-class _MockMarkersController extends Mock implements MarkersController {}
-
-class _MockGMap extends Mock implements gmaps.GMap {
-  final onClickController = StreamController<gmaps.MouseEvent>.broadcast();
-  @override
-  Stream<gmaps.MouseEvent> get onClick => onClickController.stream;
-
-  final onRightclickController = StreamController<gmaps.MouseEvent>.broadcast();
-  @override
-  Stream<gmaps.MouseEvent> get onRightclick => onRightclickController.stream;
-
-  final onBoundsChangedController = StreamController<dynamic>.broadcast();
-  @override
-  Stream<dynamic> get onBoundsChanged => onBoundsChangedController.stream;
-
-  final onIdleController = StreamController<dynamic>.broadcast();
-  @override
-  Stream<dynamic> get onIdle => onIdleController.stream;
-}
+@GenerateMocks([], customMocks: [
+  MockSpec<CirclesController>(returnNullOnMissingStub: true),
+  MockSpec<PolygonsController>(returnNullOnMissingStub: true),
+  MockSpec<PolylinesController>(returnNullOnMissingStub: true),
+  MockSpec<MarkersController>(returnNullOnMissingStub: true),
+])
 
 /// Test Google Map Controller
 void main() {
@@ -46,8 +33,8 @@
 
   group('GoogleMapController', () {
     final int mapId = 33930;
-    GoogleMapController controller;
-    StreamController<MapEvent> stream;
+    late GoogleMapController controller;
+    late StreamController<MapEvent> stream;
 
     // Creates a controller with the default mapId and stream controller, and any `options` needed.
     GoogleMapController _createController({
@@ -57,17 +44,18 @@
       Set<Polygon> polygons = const <Polygon>{},
       Set<Polyline> polylines = const <Polyline>{},
       Set<Circle> circles = const <Circle>{},
-      Map<String, dynamic> options,
+      Map<String, dynamic> options = const <String, dynamic>{},
     }) {
       return GoogleMapController(
-          mapId: mapId,
-          streamController: stream,
-          initialCameraPosition: initialCameraPosition,
-          markers: markers,
-          polygons: polygons,
-          polylines: polylines,
-          circles: circles,
-          mapOptions: options ?? <String, dynamic>{});
+        mapId: mapId,
+        streamController: stream,
+        initialCameraPosition: initialCameraPosition,
+        markers: markers,
+        polygons: polygons,
+        polylines: polylines,
+        circles: circles,
+        mapOptions: options,
+      );
     }
 
     setUp(() {
@@ -81,7 +69,9 @@
 
       testWidgets('constructor creates widget', (WidgetTester tester) async {
         expect(controller.widget, isNotNull);
-        expect(controller.widget.viewType, endsWith('$mapId'));
+        expect(controller.widget, isA<HtmlElementView>());
+        expect((controller.widget as HtmlElementView).viewType,
+            endsWith('$mapId'));
       });
 
       testWidgets('widget is cached when reused', (WidgetTester tester) async {
@@ -90,27 +80,130 @@
         expect(identical(first, again), isTrue);
       });
 
-      testWidgets('dispose closes the stream and removes the widget',
-          (WidgetTester tester) async {
-        controller.dispose();
-        expect(stream.isClosed, isTrue);
-        expect(controller.widget, isNull);
+      group('dispose', () {
+        testWidgets('closes the stream and removes the widget',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(stream.isClosed, isTrue);
+          expect(controller.widget, isNull);
+        });
+
+        testWidgets('cannot call getVisibleRegion after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() async {
+            await controller.getVisibleRegion();
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot call getScreenCoordinate after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() async {
+            await controller.getScreenCoordinate(
+              LatLng(43.3072465, -5.6918241),
+            );
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot call getLatLng after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() async {
+            await controller.getLatLng(
+              ScreenCoordinate(x: 640, y: 480),
+            );
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot call moveCamera after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() async {
+            await controller.moveCamera(CameraUpdate.zoomIn());
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot call getZoomLevel after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() async {
+            await controller.getZoomLevel();
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot updateCircles after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() {
+            controller.updateCircles(CircleUpdates.from({}, {}));
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot updatePolygons after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() {
+            controller.updatePolygons(PolygonUpdates.from({}, {}));
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot updatePolylines after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() {
+            controller.updatePolylines(PolylineUpdates.from({}, {}));
+          }, throwsAssertionError);
+        });
+
+        testWidgets('cannot updateMarkers after dispose',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(() {
+            controller.updateMarkers(MarkerUpdates.from({}, {}));
+          }, throwsAssertionError);
+
+          expect(() {
+            controller.showInfoWindow(MarkerId('any'));
+          }, throwsAssertionError);
+
+          expect(() {
+            controller.hideInfoWindow(MarkerId('any'));
+          }, throwsAssertionError);
+        });
+
+        testWidgets('isInfoWindowShown defaults to false',
+            (WidgetTester tester) async {
+          controller.dispose();
+
+          expect(controller.isInfoWindowShown(MarkerId('any')), false);
+        });
       });
     });
 
     group('init', () {
-      _MockCirclesController circles;
-      _MockMarkersController markers;
-      _MockPolygonsController polygons;
-      _MockPolylinesController polylines;
-      _MockGMap map;
+      late MockCirclesController circles;
+      late MockMarkersController markers;
+      late MockPolygonsController polygons;
+      late MockPolylinesController polylines;
+      late gmaps.GMap map;
 
       setUp(() {
-        circles = _MockCirclesController();
-        markers = _MockMarkersController();
-        polygons = _MockPolygonsController();
-        polylines = _MockPolylinesController();
-        map = _MockGMap();
+        circles = MockCirclesController();
+        markers = MockMarkersController();
+        polygons = MockPolygonsController();
+        polylines = MockPolylinesController();
+        map = gmaps.GMap(html.DivElement());
       });
 
       testWidgets('listens to map events', (WidgetTester tester) async {
@@ -123,17 +216,25 @@
           polylines: polylines,
         );
 
-        expect(map.onClickController.hasListener, isFalse);
-        expect(map.onRightclickController.hasListener, isFalse);
-        expect(map.onBoundsChangedController.hasListener, isFalse);
-        expect(map.onIdleController.hasListener, isFalse);
-
         controller.init();
 
-        expect(map.onClickController.hasListener, isTrue);
-        expect(map.onRightclickController.hasListener, isTrue);
-        expect(map.onBoundsChangedController.hasListener, isTrue);
-        expect(map.onIdleController.hasListener, isTrue);
+        // Trigger events on the map, and verify they've been broadcast to the stream
+        final capturedEvents = stream.stream.take(5);
+
+        gmaps.Event.trigger(
+            map, 'click', [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]);
+        gmaps.Event.trigger(map, 'rightclick',
+            [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]);
+        gmaps.Event.trigger(map, 'bounds_changed', []); // Causes 2 events
+        gmaps.Event.trigger(map, 'idle', []);
+
+        final events = await capturedEvents.toList();
+
+        expect(events[0], isA<MapTapEvent>());
+        expect(events[1], isA<MapLongPressEvent>());
+        expect(events[2], isA<CameraMoveStartedEvent>());
+        expect(events[3], isA<CameraMoveEvent>());
+        expect(events[4], isA<CameraIdleEvent>());
       });
 
       testWidgets('binds geometry controllers to map\'s',
@@ -227,7 +328,7 @@
       testWidgets('empty infoWindow does not create InfoWindow instance.',
           (WidgetTester tester) async {
         controller = _createController(markers: {
-          Marker(markerId: MarkerId('marker-1'), infoWindow: null),
+          Marker(markerId: MarkerId('marker-1')),
         });
 
         controller.debugSetOverrides(
@@ -239,11 +340,11 @@
         final capturedMarkers =
             verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>;
 
-        expect(capturedMarkers.first.infoWindow, isNull);
+        expect(capturedMarkers.first.infoWindow, InfoWindow.noText);
       });
 
       group('Initialization options', () {
-        gmaps.MapOptions capturedOptions;
+        gmaps.MapOptions? capturedOptions;
         setUp(() {
           capturedOptions = null;
         });
@@ -260,9 +361,9 @@
           controller.init();
 
           expect(capturedOptions, isNotNull);
-          expect(capturedOptions.mapTypeId, gmaps.MapTypeId.SATELLITE);
-          expect(capturedOptions.zoomControl, true);
-          expect(capturedOptions.gestureHandling, 'auto',
+          expect(capturedOptions!.mapTypeId, gmaps.MapTypeId.SATELLITE);
+          expect(capturedOptions!.zoomControl, true);
+          expect(capturedOptions!.gestureHandling, 'auto',
               reason:
                   'by default the map handles zoom/pan gestures internally');
         });
@@ -280,7 +381,7 @@
           controller.init();
 
           expect(capturedOptions, isNotNull);
-          expect(capturedOptions.gestureHandling, 'none',
+          expect(capturedOptions!.gestureHandling, 'none',
               reason:
                   'disabling scroll gestures disables all gesture handling');
         });
@@ -298,29 +399,11 @@
           controller.init();
 
           expect(capturedOptions, isNotNull);
-          expect(capturedOptions.gestureHandling, 'none',
+          expect(capturedOptions!.gestureHandling, 'none',
               reason:
                   'disabling scroll gestures disables all gesture handling');
         });
 
-        testWidgets('does not set initial position if absent',
-            (WidgetTester tester) async {
-          controller = _createController(
-            initialCameraPosition: null,
-          );
-
-          controller.debugSetOverrides(createMap: (_, options) {
-            capturedOptions = options;
-            return map;
-          });
-
-          controller.init();
-
-          expect(capturedOptions, isNotNull);
-          expect(capturedOptions.zoom, isNull);
-          expect(capturedOptions.center, isNull);
-        });
-
         testWidgets('sets initial position when passed',
             (WidgetTester tester) async {
           controller = _createController(
@@ -340,8 +423,8 @@
           controller.init();
 
           expect(capturedOptions, isNotNull);
-          expect(capturedOptions.zoom, 12);
-          expect(capturedOptions.center, isNotNull);
+          expect(capturedOptions!.zoom, 12);
+          expect(capturedOptions!.center, isNotNull);
         });
       });
 
@@ -366,10 +449,15 @@
 
     // These are the methods that are delegated to the gmaps.GMap object, that we can mock...
     group('Map control methods', () {
-      _MockGMap map;
+      late gmaps.GMap map;
 
       setUp(() {
-        map = _MockGMap();
+        map = gmaps.GMap(
+          html.DivElement(),
+          gmaps.MapOptions()
+            ..zoom = 10
+            ..center = gmaps.LatLng(0, 0),
+        );
         controller = _createController();
         controller.debugSetOverrides(createMap: (_, __) => map);
         controller.init();
@@ -380,9 +468,8 @@
           controller.updateRawOptions({
             'mapType': 2,
           });
-          final options = verify(map.options = captureAny).captured[0];
 
-          expect(options.mapTypeId, gmaps.MapTypeId.SATELLITE);
+          expect(map.mapTypeId, gmaps.MapTypeId.SATELLITE);
         });
 
         testWidgets('can turn on/off traffic', (WidgetTester tester) async {
@@ -404,17 +491,19 @@
 
       group('viewport getters', () {
         testWidgets('getVisibleRegion', (WidgetTester tester) async {
-          await controller.getVisibleRegion();
+          final gmCenter = map.center!;
+          final center =
+              LatLng(gmCenter.lat.toDouble(), gmCenter.lng.toDouble());
 
-          verify(map.bounds);
+          final bounds = await controller.getVisibleRegion();
+
+          expect(bounds.contains(center), isTrue,
+              reason:
+                  'The computed visible region must contain the center of the created map.');
         });
 
         testWidgets('getZoomLevel', (WidgetTester tester) async {
-          when(map.zoom).thenReturn(10);
-
-          await controller.getZoomLevel();
-
-          verify(map.zoom);
+          expect(await controller.getZoomLevel(), map.zoom);
         });
       });
 
@@ -423,10 +512,11 @@
           await (controller
               .moveCamera(CameraUpdate.newLatLngZoom(LatLng(19, 26), 12)));
 
-          verify(map.zoom = 12);
-          final captured = verify(map.panTo(captureAny)).captured[0];
-          expect(captured.lat, 19);
-          expect(captured.lng, 26);
+          final gmCenter = map.center!;
+
+          expect(map.zoom, 12);
+          expect(gmCenter.lat, closeTo(19, _acceptableDelta));
+          expect(gmCenter.lng, closeTo(26, _acceptableDelta));
         });
       });
 
@@ -445,7 +535,7 @@
       });
 
       testWidgets('updateCircles', (WidgetTester tester) async {
-        final mock = _MockCirclesController();
+        final mock = MockCirclesController();
         controller.debugSetOverrides(circles: mock);
 
         final previous = {
@@ -472,7 +562,7 @@
       });
 
       testWidgets('updateMarkers', (WidgetTester tester) async {
-        final mock = _MockMarkersController();
+        final mock = MockMarkersController();
         controller.debugSetOverrides(markers: mock);
 
         final previous = {
@@ -499,7 +589,7 @@
       });
 
       testWidgets('updatePolygons', (WidgetTester tester) async {
-        final mock = _MockPolygonsController();
+        final mock = MockPolygonsController();
         controller.debugSetOverrides(polygons: mock);
 
         final previous = {
@@ -526,7 +616,7 @@
       });
 
       testWidgets('updatePolylines', (WidgetTester tester) async {
-        final mock = _MockPolylinesController();
+        final mock = MockPolylinesController();
         controller.debugSetOverrides(polylines: mock);
 
         final previous = {
@@ -553,9 +643,10 @@
       });
 
       testWidgets('infoWindow visibility', (WidgetTester tester) async {
-        final mock = _MockMarkersController();
-        controller.debugSetOverrides(markers: mock);
+        final mock = MockMarkersController();
         final markerId = MarkerId('marker-with-infowindow');
+        when(mock.isInfoWindowShown(markerId)).thenReturn(true);
+        controller.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
new file mode 100644
index 0000000..4793328
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart
@@ -0,0 +1,202 @@
+// 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.
+
+// Mocks generated by Mockito 5.0.2 from annotations
+// in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart.
+// Do not manually edit this file.
+
+import 'package:google_maps/src/generated/google_maps_core.js.g.dart' as _i2;
+import 'package:google_maps_flutter_platform_interface/src/types/circle.dart'
+    as _i4;
+import 'package:google_maps_flutter_platform_interface/src/types/marker.dart'
+    as _i7;
+import 'package:google_maps_flutter_platform_interface/src/types/polygon.dart'
+    as _i5;
+import 'package:google_maps_flutter_platform_interface/src/types/polyline.dart'
+    as _i6;
+import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i3;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: comment_references
+// ignore_for_file: unnecessary_parenthesis
+
+class _FakeGMap extends _i1.Fake implements _i2.GMap {}
+
+/// A class which mocks [CirclesController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockCirclesController extends _i1.Mock implements _i3.CirclesController {
+  @override
+  Map<_i4.CircleId, _i3.CircleController> get circles =>
+      (super.noSuchMethod(Invocation.getter(#circles),
+              returnValue: <_i4.CircleId, _i3.CircleController>{})
+          as Map<_i4.CircleId, _i3.CircleController>);
+  @override
+  _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap),
+      returnValue: _FakeGMap()) as _i2.GMap);
+  @override
+  set googleMap(_i2.GMap? _googleMap) =>
+      super.noSuchMethod(Invocation.setter(#googleMap, _googleMap),
+          returnValueForMissingStub: null);
+  @override
+  int get mapId =>
+      (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int);
+  @override
+  set mapId(int? _mapId) =>
+      super.noSuchMethod(Invocation.setter(#mapId, _mapId),
+          returnValueForMissingStub: null);
+  @override
+  void addCircles(Set<_i4.Circle>? circlesToAdd) =>
+      super.noSuchMethod(Invocation.method(#addCircles, [circlesToAdd]),
+          returnValueForMissingStub: null);
+  @override
+  void changeCircles(Set<_i4.Circle>? circlesToChange) =>
+      super.noSuchMethod(Invocation.method(#changeCircles, [circlesToChange]),
+          returnValueForMissingStub: null);
+  @override
+  void removeCircles(Set<_i4.CircleId>? circleIdsToRemove) =>
+      super.noSuchMethod(Invocation.method(#removeCircles, [circleIdsToRemove]),
+          returnValueForMissingStub: null);
+  @override
+  void bindToMap(int? mapId, _i2.GMap? googleMap) =>
+      super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]),
+          returnValueForMissingStub: null);
+}
+
+/// A class which mocks [PolygonsController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPolygonsController extends _i1.Mock
+    implements _i3.PolygonsController {
+  @override
+  Map<_i5.PolygonId, _i3.PolygonController> get polygons =>
+      (super.noSuchMethod(Invocation.getter(#polygons),
+              returnValue: <_i5.PolygonId, _i3.PolygonController>{})
+          as Map<_i5.PolygonId, _i3.PolygonController>);
+  @override
+  _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap),
+      returnValue: _FakeGMap()) as _i2.GMap);
+  @override
+  set googleMap(_i2.GMap? _googleMap) =>
+      super.noSuchMethod(Invocation.setter(#googleMap, _googleMap),
+          returnValueForMissingStub: null);
+  @override
+  int get mapId =>
+      (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int);
+  @override
+  set mapId(int? _mapId) =>
+      super.noSuchMethod(Invocation.setter(#mapId, _mapId),
+          returnValueForMissingStub: null);
+  @override
+  void addPolygons(Set<_i5.Polygon>? polygonsToAdd) =>
+      super.noSuchMethod(Invocation.method(#addPolygons, [polygonsToAdd]),
+          returnValueForMissingStub: null);
+  @override
+  void changePolygons(Set<_i5.Polygon>? polygonsToChange) =>
+      super.noSuchMethod(Invocation.method(#changePolygons, [polygonsToChange]),
+          returnValueForMissingStub: null);
+  @override
+  void removePolygons(Set<_i5.PolygonId>? polygonIdsToRemove) => super
+      .noSuchMethod(Invocation.method(#removePolygons, [polygonIdsToRemove]),
+          returnValueForMissingStub: null);
+  @override
+  void bindToMap(int? mapId, _i2.GMap? googleMap) =>
+      super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]),
+          returnValueForMissingStub: null);
+}
+
+/// A class which mocks [PolylinesController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPolylinesController extends _i1.Mock
+    implements _i3.PolylinesController {
+  @override
+  Map<_i6.PolylineId, _i3.PolylineController> get lines =>
+      (super.noSuchMethod(Invocation.getter(#lines),
+              returnValue: <_i6.PolylineId, _i3.PolylineController>{})
+          as Map<_i6.PolylineId, _i3.PolylineController>);
+  @override
+  _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap),
+      returnValue: _FakeGMap()) as _i2.GMap);
+  @override
+  set googleMap(_i2.GMap? _googleMap) =>
+      super.noSuchMethod(Invocation.setter(#googleMap, _googleMap),
+          returnValueForMissingStub: null);
+  @override
+  int get mapId =>
+      (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int);
+  @override
+  set mapId(int? _mapId) =>
+      super.noSuchMethod(Invocation.setter(#mapId, _mapId),
+          returnValueForMissingStub: null);
+  @override
+  void addPolylines(Set<_i6.Polyline>? polylinesToAdd) =>
+      super.noSuchMethod(Invocation.method(#addPolylines, [polylinesToAdd]),
+          returnValueForMissingStub: null);
+  @override
+  void changePolylines(Set<_i6.Polyline>? polylinesToChange) => super
+      .noSuchMethod(Invocation.method(#changePolylines, [polylinesToChange]),
+          returnValueForMissingStub: null);
+  @override
+  void removePolylines(Set<_i6.PolylineId>? polylineIdsToRemove) => super
+      .noSuchMethod(Invocation.method(#removePolylines, [polylineIdsToRemove]),
+          returnValueForMissingStub: null);
+  @override
+  void bindToMap(int? mapId, _i2.GMap? googleMap) =>
+      super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]),
+          returnValueForMissingStub: null);
+}
+
+/// A class which mocks [MarkersController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockMarkersController extends _i1.Mock implements _i3.MarkersController {
+  @override
+  Map<_i7.MarkerId, _i3.MarkerController> get markers =>
+      (super.noSuchMethod(Invocation.getter(#markers),
+              returnValue: <_i7.MarkerId, _i3.MarkerController>{})
+          as Map<_i7.MarkerId, _i3.MarkerController>);
+  @override
+  _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap),
+      returnValue: _FakeGMap()) as _i2.GMap);
+  @override
+  set googleMap(_i2.GMap? _googleMap) =>
+      super.noSuchMethod(Invocation.setter(#googleMap, _googleMap),
+          returnValueForMissingStub: null);
+  @override
+  int get mapId =>
+      (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int);
+  @override
+  set mapId(int? _mapId) =>
+      super.noSuchMethod(Invocation.setter(#mapId, _mapId),
+          returnValueForMissingStub: null);
+  @override
+  void addMarkers(Set<_i7.Marker>? markersToAdd) =>
+      super.noSuchMethod(Invocation.method(#addMarkers, [markersToAdd]),
+          returnValueForMissingStub: null);
+  @override
+  void changeMarkers(Set<_i7.Marker>? markersToChange) =>
+      super.noSuchMethod(Invocation.method(#changeMarkers, [markersToChange]),
+          returnValueForMissingStub: null);
+  @override
+  void removeMarkers(Set<_i7.MarkerId>? markerIdsToRemove) =>
+      super.noSuchMethod(Invocation.method(#removeMarkers, [markerIdsToRemove]),
+          returnValueForMissingStub: null);
+  @override
+  void showMarkerInfoWindow(_i7.MarkerId? markerId) =>
+      super.noSuchMethod(Invocation.method(#showMarkerInfoWindow, [markerId]),
+          returnValueForMissingStub: null);
+  @override
+  void hideMarkerInfoWindow(_i7.MarkerId? markerId) =>
+      super.noSuchMethod(Invocation.method(#hideMarkerInfoWindow, [markerId]),
+          returnValueForMissingStub: null);
+  @override
+  bool isInfoWindowShown(_i7.MarkerId? markerId) =>
+      (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]),
+          returnValue: false) as bool);
+  @override
+  void bindToMap(int? mapId, _i2.GMap? googleMap) =>
+      super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]),
+          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 dee3761..2de431a 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
@@ -2,36 +2,40 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.9
-
 import 'dart:async';
+import 'dart:js_util' show getProperty;
 
 import 'package:integration_test/integration_test.dart';
 import 'package:flutter/widgets.dart';
 import 'package:google_maps/google_maps.dart' as gmaps;
 import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
 
 import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
 
-class _MockGoogleMapController extends Mock implements GoogleMapController {}
+import 'google_maps_plugin_test.mocks.dart';
+
+@GenerateMocks([], customMocks: [
+  MockSpec<GoogleMapController>(returnNullOnMissingStub: true),
+])
 
 /// Test GoogleMapsPlugin
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
   group('GoogleMapsPlugin', () {
-    _MockGoogleMapController controller;
-    GoogleMapsPlugin plugin;
-    int reportedMapId;
+    late MockGoogleMapController controller;
+    late GoogleMapsPlugin plugin;
+    int? reportedMapId;
 
     void onPlatformViewCreated(int id) {
       reportedMapId = id;
     }
 
     setUp(() {
-      controller = _MockGoogleMapController();
+      controller = MockGoogleMapController();
       plugin = GoogleMapsPlugin();
       reportedMapId = null;
     });
@@ -72,34 +76,21 @@
       final testMapId = 33930;
       final initialCameraPosition = CameraPosition(target: LatLng(0, 0));
 
-      testWidgets('throws without _webOnlyMapCreationId',
-          (WidgetTester tester) async {
-        expect(
-          () => plugin.buildView(
-            null,
-            onPlatformViewCreated,
-            initialCameraPosition: initialCameraPosition,
-          ),
-          throwsAssertionError,
-          reason:
-              '_webOnlyMapCreationId is mandatory to prevent unnecessary reloads in web.',
-        );
-      });
-
       testWidgets(
           'returns an HtmlElementView and caches the controller for later',
           (WidgetTester tester) async {
         final Map<int, GoogleMapController> cache = {};
         plugin.debugSetMapById(cache);
 
-        final HtmlElementView widget = plugin.buildView(
+        final Widget widget = plugin.buildView(
           testMapId,
           onPlatformViewCreated,
           initialCameraPosition: initialCameraPosition,
         );
 
+        expect(widget, isA<HtmlElementView>());
         expect(
-          widget.viewType,
+          (widget as HtmlElementView).viewType,
           contains('$testMapId'),
           reason:
               'view type should contain the mapId passed when creating the map.',
@@ -160,11 +151,10 @@
         expect(styles.length, 1);
         // Let's peek inside the styles...
         var style = styles[0] as gmaps.MapTypeStyle;
-        expect(style.featureType, gmaps.MapTypeStyleFeatureType.POI_PARK);
-        expect(
-            style.elementType, gmaps.MapTypeStyleElementType.LABELS_TEXT_FILL);
-        expect(style.stylers.length, 1);
-        expect(style.stylers[0].color, '#6b9a76');
+        expect(style.featureType, 'poi.park');
+        expect(style.elementType, 'labels.text.fill');
+        expect(style.stylers?.length, 1);
+        expect(getProperty(style.stylers![0]!, 'color'), '#6b9a76');
       });
     });
 
@@ -247,31 +237,50 @@
 
         verify(controller.moveCamera(expectedUpdates));
       });
+
       // Viewport
       testWidgets('getVisibleRegion', (WidgetTester tester) async {
+        when(controller.getVisibleRegion())
+            .thenAnswer((_) async => LatLngBounds(
+                  northeast: LatLng(47.2359634, -68.0192019),
+                  southwest: LatLng(34.5019594, -120.4974629),
+                ));
         await plugin.getVisibleRegion(mapId: mapId);
 
         verify(controller.getVisibleRegion());
       });
+
       testWidgets('getZoomLevel', (WidgetTester tester) async {
+        when(controller.getZoomLevel()).thenAnswer((_) async => 10);
         await plugin.getZoomLevel(mapId: mapId);
 
         verify(controller.getZoomLevel());
       });
+
       testWidgets('getScreenCoordinate', (WidgetTester tester) async {
+        when(controller.getScreenCoordinate(any)).thenAnswer(
+            (_) async => ScreenCoordinate(x: 320, y: 240) // fake return
+            );
+
         final latLng = LatLng(43.3613, -5.8499);
 
         await plugin.getScreenCoordinate(latLng, mapId: mapId);
 
         verify(controller.getScreenCoordinate(latLng));
       });
+
       testWidgets('getLatLng', (WidgetTester tester) async {
+        when(controller.getLatLng(any))
+            .thenAnswer((_) async => LatLng(43.3613, -5.8499) // fake return
+                );
+
         final coordinates = ScreenCoordinate(x: 19, y: 26);
 
         await plugin.getLatLng(coordinates, mapId: mapId);
 
         verify(controller.getLatLng(coordinates));
       });
+
       // InfoWindows
       testWidgets('showMarkerInfoWindow', (WidgetTester tester) async {
         final markerId = MarkerId('testing-123');
@@ -280,6 +289,7 @@
 
         verify(controller.showInfoWindow(markerId));
       });
+
       testWidgets('hideMarkerInfoWindow', (WidgetTester tester) async {
         final markerId = MarkerId('testing-123');
 
@@ -287,7 +297,10 @@
 
         verify(controller.hideInfoWindow(markerId));
       });
+
       testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async {
+        when(controller.isInfoWindowShown(any)).thenReturn(true);
+
         final markerId = MarkerId('testing-123');
 
         await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId);
@@ -299,7 +312,7 @@
     // Verify all event streams are filtered correctly from the main one...
     group('Event Streams', () {
       int mapId = 0;
-      StreamController<MapEvent> streamController;
+      late StreamController<MapEvent> streamController;
       setUp(() {
         streamController = StreamController<MapEvent>.broadcast();
         when(controller.events)
@@ -308,7 +321,8 @@
       });
 
       // Dispatches a few events in the global streamController, and expects *only* the passed event to be there.
-      void _testStreamFiltering(Stream<MapEvent> stream, MapEvent event) async {
+      Future<void> _testStreamFiltering(
+          Stream<MapEvent> stream, MapEvent event) async {
         Timer.run(() {
           streamController.add(_OtherMapEvent(mapId));
           streamController.add(event);
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
new file mode 100644
index 0000000..43150f6
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart
@@ -0,0 +1,106 @@
+// 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.
+
+// Mocks generated by Mockito 5.0.2 from annotations
+// in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart.
+// Do not manually edit this file.
+
+import 'dart:async' as _i5;
+
+import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart'
+    as _i6;
+import 'package:google_maps_flutter_platform_interface/src/types/camera.dart'
+    as _i7;
+import 'package:google_maps_flutter_platform_interface/src/types/circle_updates.dart'
+    as _i8;
+import 'package:google_maps_flutter_platform_interface/src/types/location.dart'
+    as _i2;
+import 'package:google_maps_flutter_platform_interface/src/types/marker.dart'
+    as _i12;
+import 'package:google_maps_flutter_platform_interface/src/types/marker_updates.dart'
+    as _i11;
+import 'package:google_maps_flutter_platform_interface/src/types/polygon_updates.dart'
+    as _i9;
+import 'package:google_maps_flutter_platform_interface/src/types/polyline_updates.dart'
+    as _i10;
+import 'package:google_maps_flutter_platform_interface/src/types/screen_coordinate.dart'
+    as _i3;
+import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: comment_references
+// ignore_for_file: unnecessary_parenthesis
+
+class _FakeLatLngBounds extends _i1.Fake implements _i2.LatLngBounds {}
+
+class _FakeScreenCoordinate extends _i1.Fake implements _i3.ScreenCoordinate {}
+
+class _FakeLatLng extends _i1.Fake implements _i2.LatLng {}
+
+/// A class which mocks [GoogleMapController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockGoogleMapController extends _i1.Mock
+    implements _i4.GoogleMapController {
+  @override
+  _i5.Stream<_i6.MapEvent<dynamic>> get events =>
+      (super.noSuchMethod(Invocation.getter(#events),
+              returnValue: Stream<_i6.MapEvent<dynamic>>.empty())
+          as _i5.Stream<_i6.MapEvent<dynamic>>);
+  @override
+  void updateRawOptions(Map<String, dynamic>? optionsUpdate) =>
+      super.noSuchMethod(Invocation.method(#updateRawOptions, [optionsUpdate]),
+          returnValueForMissingStub: null);
+  @override
+  _i5.Future<_i2.LatLngBounds> getVisibleRegion() =>
+      (super.noSuchMethod(Invocation.method(#getVisibleRegion, []),
+              returnValue: Future.value(_FakeLatLngBounds()))
+          as _i5.Future<_i2.LatLngBounds>);
+  @override
+  _i5.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i2.LatLng? latLng) =>
+      (super.noSuchMethod(Invocation.method(#getScreenCoordinate, [latLng]),
+              returnValue: Future.value(_FakeScreenCoordinate()))
+          as _i5.Future<_i3.ScreenCoordinate>);
+  @override
+  _i5.Future<_i2.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) =>
+      (super.noSuchMethod(Invocation.method(#getLatLng, [screenCoordinate]),
+          returnValue: Future.value(_FakeLatLng())) as _i5.Future<_i2.LatLng>);
+  @override
+  _i5.Future<void> moveCamera(_i7.CameraUpdate? cameraUpdate) =>
+      (super.noSuchMethod(Invocation.method(#moveCamera, [cameraUpdate]),
+          returnValue: Future.value(null),
+          returnValueForMissingStub: Future.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<double> getZoomLevel() =>
+      (super.noSuchMethod(Invocation.method(#getZoomLevel, []),
+          returnValue: Future.value(0.0)) as _i5.Future<double>);
+  @override
+  void updateCircles(_i8.CircleUpdates? updates) =>
+      super.noSuchMethod(Invocation.method(#updateCircles, [updates]),
+          returnValueForMissingStub: null);
+  @override
+  void updatePolygons(_i9.PolygonUpdates? updates) =>
+      super.noSuchMethod(Invocation.method(#updatePolygons, [updates]),
+          returnValueForMissingStub: null);
+  @override
+  void updatePolylines(_i10.PolylineUpdates? updates) =>
+      super.noSuchMethod(Invocation.method(#updatePolylines, [updates]),
+          returnValueForMissingStub: null);
+  @override
+  void updateMarkers(_i11.MarkerUpdates? updates) =>
+      super.noSuchMethod(Invocation.method(#updateMarkers, [updates]),
+          returnValueForMissingStub: null);
+  @override
+  void showInfoWindow(_i12.MarkerId? markerId) =>
+      super.noSuchMethod(Invocation.method(#showInfoWindow, [markerId]),
+          returnValueForMissingStub: null);
+  @override
+  void hideInfoWindow(_i12.MarkerId? markerId) =>
+      super.noSuchMethod(Invocation.method(#hideInfoWindow, [markerId]),
+          returnValueForMissingStub: null);
+  @override
+  bool isInfoWindowShown(_i12.MarkerId? markerId) =>
+      (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]),
+          returnValue: false) as bool);
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart
index 1a85f3b..2bfa27b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart
@@ -2,100 +2,157 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.9
-
 import 'dart:async';
+import 'dart:html' as html;
 
 import 'package:integration_test/integration_test.dart';
 import 'package:google_maps/google_maps.dart' as gmaps;
 import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-
-class _MockMarker extends Mock implements gmaps.Marker {
-  final onClickController = StreamController<gmaps.MouseEvent>();
-  final onDragEndController = StreamController<gmaps.MouseEvent>();
-
-  @override
-  Stream<gmaps.MouseEvent> get onClick => onClickController.stream;
-
-  @override
-  Stream<gmaps.MouseEvent> get onDragend => onDragEndController.stream;
-}
-
-class _MockMouseEvent extends Mock implements gmaps.MouseEvent {}
-
-class _MockInfoWindow extends Mock implements gmaps.InfoWindow {}
 
 /// Test Markers
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
-  bool called = false;
+  // Since onTap/DragEnd events happen asynchronously, we need to store when the event
+  // is fired. We use a completer so the test can wait for the future to be completed.
+  late Completer<bool> _methodCalledCompleter;
+
+  /// This is the future value of the [_methodCalledCompleter]. Reinitialized
+  /// in the [setUp] method, and completed (as `true`) by [onTap] and [onDragEnd]
+  /// when those methods are called from the MarkerController.
+  late Future<bool> methodCalled;
+
   void onTap() {
-    called = true;
+    _methodCalledCompleter.complete(true);
   }
 
   void onDragEnd(gmaps.LatLng _) {
-    called = true;
+    _methodCalledCompleter.complete(true);
   }
 
   setUp(() {
-    called = false;
+    _methodCalledCompleter = Completer();
+    methodCalled = _methodCalledCompleter.future;
   });
 
   group('MarkerController', () {
-    _MockMarker marker;
+    late gmaps.Marker marker;
 
     setUp(() {
-      marker = _MockMarker();
+      marker = gmaps.Marker();
     });
 
     testWidgets('onTap gets called', (WidgetTester tester) async {
       MarkerController(marker: marker, onTap: onTap);
-      // Simulate a click
-      await marker.onClickController.add(null);
-      expect(called, isTrue);
+
+      // Trigger a click event...
+      gmaps.Event.trigger(marker, 'click', [gmaps.MapMouseEvent()]);
+
+      // The event handling is now truly async. Wait for it...
+      expect(await methodCalled, isTrue);
     });
 
     testWidgets('onDragEnd gets called', (WidgetTester tester) async {
-      when(marker.draggable).thenReturn(true);
       MarkerController(marker: marker, onDragEnd: onDragEnd);
-      // Simulate a drag end
-      await marker.onDragEndController.add(_MockMouseEvent());
-      expect(called, isTrue);
+
+      // Trigger a drag end event...
+      gmaps.Event.trigger(marker, 'dragend',
+          [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]);
+
+      expect(await methodCalled, isTrue);
     });
 
     testWidgets('update', (WidgetTester tester) async {
       final controller = MarkerController(marker: marker);
-      final options = gmaps.MarkerOptions()..draggable = false;
+      final options = gmaps.MarkerOptions()..draggable = true;
+
+      expect(marker.draggable, isNull);
+
       controller.update(options);
-      verify(marker.options = options);
+
+      expect(marker.draggable, isTrue);
     });
 
     testWidgets('infoWindow null, showInfoWindow.',
         (WidgetTester tester) async {
       final controller = MarkerController(marker: marker);
+
       controller.showInfoWindow();
+
       expect(controller.infoWindowShown, isFalse);
     });
 
     testWidgets('showInfoWindow', (WidgetTester tester) async {
-      final infoWindow = _MockInfoWindow();
+      final infoWindow = gmaps.InfoWindow();
+      final map = gmaps.GMap(html.DivElement());
+      marker.set('map', map);
       final controller =
           MarkerController(marker: marker, infoWindow: infoWindow);
+
       controller.showInfoWindow();
-      verify(infoWindow.open(any, any)).called(1);
+
+      expect(infoWindow.get('map'), map);
       expect(controller.infoWindowShown, isTrue);
     });
 
     testWidgets('hideInfoWindow', (WidgetTester tester) async {
-      final infoWindow = _MockInfoWindow();
+      final infoWindow = gmaps.InfoWindow();
+      final map = gmaps.GMap(html.DivElement());
+      marker.set('map', map);
       final controller =
           MarkerController(marker: marker, infoWindow: infoWindow);
+
       controller.hideInfoWindow();
-      verify(infoWindow.close()).called(1);
+
+      expect(infoWindow.get('map'), isNull);
       expect(controller.infoWindowShown, isFalse);
     });
+
+    group('remove', () {
+      late MarkerController controller;
+
+      setUp(() {
+        final infoWindow = gmaps.InfoWindow();
+        final map = gmaps.GMap(html.DivElement());
+        marker.set('map', map);
+        controller = MarkerController(marker: marker, infoWindow: infoWindow);
+      });
+
+      testWidgets('drops gmaps instance', (WidgetTester tester) async {
+        controller.remove();
+
+        expect(controller.marker, isNull);
+      });
+
+      testWidgets('cannot call update after remove',
+          (WidgetTester tester) async {
+        final options = gmaps.MarkerOptions()..draggable = true;
+
+        controller.remove();
+
+        expect(() {
+          controller.update(options);
+        }, throwsAssertionError);
+      });
+
+      testWidgets('cannot call showInfoWindow after remove',
+          (WidgetTester tester) async {
+        controller.remove();
+
+        expect(() {
+          controller.showInfoWindow();
+        }, throwsAssertionError);
+      });
+
+      testWidgets('cannot call hideInfoWindow after remove',
+          (WidgetTester tester) async {
+        controller.remove();
+
+        expect(() {
+          controller.hideInfoWindow();
+        }, throwsAssertionError);
+      });
+    });
   });
 }
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 e9e458c..6f2bf61 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
@@ -2,17 +2,17 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.9
-
 import 'dart:async';
 import 'dart:convert';
-import 'dart:html';
+import 'dart:html' as html;
+import 'dart:js_util' show getProperty;
 
-import 'package:http/http.dart' as http;
-import 'package:integration_test/integration_test.dart';
+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:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:integration_test/integration_test.dart';
 
 import 'resources/icon_image_base64.dart';
 
@@ -20,12 +20,15 @@
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
   group('MarkersController', () {
-    StreamController<MapEvent> stream;
-    MarkersController controller;
+    late StreamController<MapEvent> events;
+    late MarkersController controller;
+    late gmaps.GMap map;
 
     setUp(() {
-      stream = StreamController<MapEvent>();
-      controller = MarkersController(stream: stream);
+      events = StreamController<MapEvent>();
+      controller = MarkersController(stream: events);
+      map = gmaps.GMap(html.DivElement());
+      controller.bindToMap(123, map);
     });
 
     testWidgets('addMarkers', (WidgetTester tester) async {
@@ -48,7 +51,7 @@
       };
       controller.addMarkers(markers);
 
-      expect(controller.markers[MarkerId('1')].marker.draggable, isFalse);
+      expect(controller.markers[MarkerId('1')]?.marker?.draggable, isFalse);
 
       // Update the marker with radius 10
       final updatedMarkers = {
@@ -57,7 +60,7 @@
       controller.changeMarkers(updatedMarkers);
 
       expect(controller.markers.length, 1);
-      expect(controller.markers[MarkerId('1')].marker.draggable, isTrue);
+      expect(controller.markers[MarkerId('1')]?.marker?.draggable, isTrue);
     });
 
     testWidgets('removeMarkers', (WidgetTester tester) async {
@@ -95,15 +98,15 @@
 
       controller.addMarkers(markers);
 
-      expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse);
+      expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse);
 
       controller.showMarkerInfoWindow(MarkerId('1'));
 
-      expect(controller.markers[MarkerId('1')].infoWindowShown, isTrue);
+      expect(controller.markers[MarkerId('1')]?.infoWindowShown, isTrue);
 
       controller.hideMarkerInfoWindow(MarkerId('1'));
 
-      expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse);
+      expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse);
     });
 
     // https://github.com/flutter/flutter/issues/67380
@@ -121,33 +124,21 @@
       };
       controller.addMarkers(markers);
 
-      expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse);
-      expect(controller.markers[MarkerId('2')].infoWindowShown, isFalse);
+      expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse);
+      expect(controller.markers[MarkerId('2')]?.infoWindowShown, isFalse);
 
       controller.showMarkerInfoWindow(MarkerId('1'));
 
-      expect(controller.markers[MarkerId('1')].infoWindowShown, isTrue);
-      expect(controller.markers[MarkerId('2')].infoWindowShown, isFalse);
+      expect(controller.markers[MarkerId('1')]?.infoWindowShown, isTrue);
+      expect(controller.markers[MarkerId('2')]?.infoWindowShown, isFalse);
 
       controller.showMarkerInfoWindow(MarkerId('2'));
 
-      expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse);
-      expect(controller.markers[MarkerId('2')].infoWindowShown, isTrue);
+      expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse);
+      expect(controller.markers[MarkerId('2')]?.infoWindowShown, isTrue);
     });
 
-    // https://github.com/flutter/flutter/issues/64938
-    testWidgets('markers with icon:null work', (WidgetTester tester) async {
-      final markers = {
-        Marker(markerId: MarkerId('1'), icon: null),
-      };
-
-      controller.addMarkers(markers);
-
-      expect(controller.markers.length, 1);
-      expect(controller.markers[MarkerId('1')].marker.icon, isNull);
-    });
-
-    //
+    // https://github.com/flutter/flutter/issues/66622
     testWidgets('markers with custom bitmap icon work',
         (WidgetTester tester) async {
       final bytes = Base64Decoder().convert(iconImageBase64);
@@ -159,11 +150,15 @@
       controller.addMarkers(markers);
 
       expect(controller.markers.length, 1);
-      expect(controller.markers[MarkerId('1')].marker.icon, isNotNull);
-      expect(controller.markers[MarkerId('1')].marker.icon.url,
-          startsWith('blob:'));
+      expect(controller.markers[MarkerId('1')]?.marker?.icon, isNotNull);
 
-      final blobUrl = controller.markers[MarkerId('1')].marker.icon.url;
+      final blobUrl = getProperty(
+        controller.markers[MarkerId('1')]!.marker!.icon!,
+        'url',
+      );
+
+      expect(blobUrl, startsWith('blob:'));
+
       final response = await http.get(Uri.parse(blobUrl));
 
       expect(response.bodyBytes, bytes,
@@ -187,8 +182,8 @@
       controller.addMarkers(markers);
 
       expect(controller.markers.length, 1);
-      final content =
-          controller.markers[MarkerId('1')].infoWindow.content as HtmlElement;
+      final content = controller.markers[MarkerId('1')]?.infoWindow?.content
+          as html.HtmlElement;
       expect(content.innerHtml, contains('title for test'));
       expect(
           content.innerHtml,
@@ -211,12 +206,12 @@
       controller.addMarkers(markers);
 
       expect(controller.markers.length, 1);
-      final content =
-          controller.markers[MarkerId('1')].infoWindow.content as HtmlElement;
+      final content = controller.markers[MarkerId('1')]?.infoWindow?.content
+          as html.HtmlElement;
 
       content.click();
 
-      final event = await stream.stream.first;
+      final event = await events.stream.first;
 
       expect(event, isA<InfoWindowTapEvent>());
       expect((event as InfoWindowTapEvent).value, equals(MarkerId('1')));
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart
index 0c35197..547aaec 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart
@@ -2,114 +2,195 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.9
-
 import 'dart:async';
 
 import 'package:integration_test/integration_test.dart';
 import 'package:google_maps/google_maps.dart' as gmaps;
 import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-
-class _MockCircle extends Mock implements gmaps.Circle {
-  final onClickController = StreamController<gmaps.MouseEvent>();
-  @override
-  Stream<gmaps.MouseEvent> get onClick => onClickController.stream;
-}
-
-class _MockPolygon extends Mock implements gmaps.Polygon {
-  final onClickController = StreamController<gmaps.PolyMouseEvent>();
-  @override
-  Stream<gmaps.PolyMouseEvent> get onClick => onClickController.stream;
-}
-
-class _MockPolyline extends Mock implements gmaps.Polyline {
-  final onClickController = StreamController<gmaps.PolyMouseEvent>();
-  @override
-  Stream<gmaps.PolyMouseEvent> get onClick => onClickController.stream;
-}
 
 /// Test Shapes (Circle, Polygon, Polyline)
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
-  bool called = false;
+  // Since onTap events happen asynchronously, we need to store when the event
+  // is fired. We use a completer so the test can wait for the future to be completed.
+  late Completer<bool> _methodCalledCompleter;
+
+  /// This is the future value of the [_methodCalledCompleter]. Reinitialized
+  /// in the [setUp] method, and completed (as `true`) by [onTap], when it gets
+  /// called by the corresponding Shape Controller.
+  late Future<bool> methodCalled;
+
   void onTap() {
-    called = true;
+    _methodCalledCompleter.complete(true);
   }
 
   setUp(() {
-    called = false;
+    _methodCalledCompleter = Completer();
+    methodCalled = _methodCalledCompleter.future;
   });
 
   group('CircleController', () {
-    _MockCircle circle;
+    late gmaps.Circle circle;
 
     setUp(() {
-      circle = _MockCircle();
+      circle = gmaps.Circle();
     });
 
     testWidgets('onTap gets called', (WidgetTester tester) async {
       CircleController(circle: circle, consumeTapEvents: true, onTap: onTap);
-      expect(circle.onClickController.hasListener, isTrue);
-      // Simulate a click
-      await circle.onClickController.add(null);
-      expect(called, isTrue);
+
+      // Trigger a click event...
+      gmaps.Event.trigger(circle, 'click', [gmaps.MapMouseEvent()]);
+
+      // The event handling is now truly async. Wait for it...
+      expect(await methodCalled, isTrue);
     });
 
     testWidgets('update', (WidgetTester tester) async {
       final controller = CircleController(circle: circle);
-      final options = gmaps.CircleOptions()..draggable = false;
+      final options = gmaps.CircleOptions()..draggable = true;
+
+      expect(circle.draggable, isNull);
+
       controller.update(options);
-      verify(circle.options = options);
+
+      expect(circle.draggable, isTrue);
+    });
+
+    group('remove', () {
+      late CircleController controller;
+
+      setUp(() {
+        controller = CircleController(circle: circle);
+      });
+
+      testWidgets('drops gmaps instance', (WidgetTester tester) async {
+        controller.remove();
+
+        expect(controller.circle, isNull);
+      });
+
+      testWidgets('cannot call update after remove',
+          (WidgetTester tester) async {
+        final options = gmaps.CircleOptions()..draggable = true;
+
+        controller.remove();
+
+        expect(() {
+          controller.update(options);
+        }, throwsAssertionError);
+      });
     });
   });
 
   group('PolygonController', () {
-    _MockPolygon polygon;
+    late gmaps.Polygon polygon;
 
     setUp(() {
-      polygon = _MockPolygon();
+      polygon = gmaps.Polygon();
     });
 
     testWidgets('onTap gets called', (WidgetTester tester) async {
       PolygonController(polygon: polygon, consumeTapEvents: true, onTap: onTap);
-      expect(polygon.onClickController.hasListener, isTrue);
-      // Simulate a click
-      await polygon.onClickController.add(null);
-      expect(called, isTrue);
+
+      // Trigger a click event...
+      gmaps.Event.trigger(polygon, 'click', [gmaps.MapMouseEvent()]);
+
+      // The event handling is now truly async. Wait for it...
+      expect(await methodCalled, isTrue);
     });
 
     testWidgets('update', (WidgetTester tester) async {
       final controller = PolygonController(polygon: polygon);
-      final options = gmaps.PolygonOptions()..draggable = false;
+      final options = gmaps.PolygonOptions()..draggable = true;
+
+      expect(polygon.draggable, isNull);
+
       controller.update(options);
-      verify(polygon.options = options);
+
+      expect(polygon.draggable, isTrue);
+    });
+
+    group('remove', () {
+      late PolygonController controller;
+
+      setUp(() {
+        controller = PolygonController(polygon: polygon);
+      });
+
+      testWidgets('drops gmaps instance', (WidgetTester tester) async {
+        controller.remove();
+
+        expect(controller.polygon, isNull);
+      });
+
+      testWidgets('cannot call update after remove',
+          (WidgetTester tester) async {
+        final options = gmaps.PolygonOptions()..draggable = true;
+
+        controller.remove();
+
+        expect(() {
+          controller.update(options);
+        }, throwsAssertionError);
+      });
     });
   });
 
   group('PolylineController', () {
-    _MockPolyline polyline;
+    late gmaps.Polyline polyline;
 
     setUp(() {
-      polyline = _MockPolyline();
+      polyline = gmaps.Polyline();
     });
 
     testWidgets('onTap gets called', (WidgetTester tester) async {
       PolylineController(
           polyline: polyline, consumeTapEvents: true, onTap: onTap);
-      expect(polyline.onClickController.hasListener, isTrue);
-      // Simulate a click
-      await polyline.onClickController.add(null);
-      expect(called, isTrue);
+
+      // Trigger a click event...
+      gmaps.Event.trigger(polyline, 'click', [gmaps.MapMouseEvent()]);
+
+      // The event handling is now truly async. Wait for it...
+      expect(await methodCalled, isTrue);
     });
 
     testWidgets('update', (WidgetTester tester) async {
       final controller = PolylineController(polyline: polyline);
-      final options = gmaps.PolylineOptions()..draggable = false;
+      final options = gmaps.PolylineOptions()..draggable = true;
+
+      expect(polyline.draggable, isNull);
+
       controller.update(options);
-      verify(polyline.options = options);
+
+      expect(polyline.draggable, isTrue);
+    });
+
+    group('remove', () {
+      late PolylineController controller;
+
+      setUp(() {
+        controller = PolylineController(polyline: polyline);
+      });
+
+      testWidgets('drops gmaps instance', (WidgetTester tester) async {
+        controller.remove();
+
+        expect(controller.line, isNull);
+      });
+
+      testWidgets('cannot call update after remove',
+          (WidgetTester tester) async {
+        final options = gmaps.PolylineOptions()..draggable = true;
+
+        controller.remove();
+
+        expect(() {
+          controller.update(options);
+        }, throwsAssertionError);
+      });
     });
   });
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart
index 9bb3599..80b4e08 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart
@@ -2,10 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.9
-
 import 'dart:async';
 import 'dart:ui';
+import 'dart:html' as html;
 
 import 'package:integration_test/integration_test.dart';
 import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
@@ -23,13 +22,20 @@
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
+  late gmaps.GMap map;
+
+  setUp(() {
+    map = gmaps.GMap(html.DivElement());
+  });
+
   group('CirclesController', () {
-    StreamController<MapEvent> stream;
-    CirclesController controller;
+    late StreamController<MapEvent> events;
+    late CirclesController controller;
 
     setUp(() {
-      stream = StreamController<MapEvent>();
-      controller = CirclesController(stream: stream);
+      events = StreamController<MapEvent>();
+      controller = CirclesController(stream: events);
+      controller.bindToMap(123, map);
     });
 
     testWidgets('addCircles', (WidgetTester tester) async {
@@ -52,7 +58,7 @@
       };
       controller.addCircles(circles);
 
-      expect(controller.circles[CircleId('1')].circle.visible, isTrue);
+      expect(controller.circles[CircleId('1')]?.circle?.visible, isTrue);
 
       final updatedCircles = {
         Circle(circleId: CircleId('1'), visible: false),
@@ -60,7 +66,7 @@
       controller.changeCircles(updatedCircles);
 
       expect(controller.circles.length, 1);
-      expect(controller.circles[CircleId('1')].circle.visible, isFalse);
+      expect(controller.circles[CircleId('1')]?.circle?.visible, isFalse);
     });
 
     testWidgets('removeCircles', (WidgetTester tester) async {
@@ -99,7 +105,7 @@
 
       controller.addCircles(circles);
 
-      final circle = controller.circles.values.first.circle;
+      final circle = controller.circles.values.first.circle!;
 
       expect(circle.get('fillColor'), '#fabada');
       expect(circle.get('fillOpacity'), closeTo(0.5, _acceptableDelta));
@@ -109,12 +115,13 @@
   });
 
   group('PolygonsController', () {
-    StreamController<MapEvent> stream;
-    PolygonsController controller;
+    late StreamController<MapEvent> events;
+    late PolygonsController controller;
 
     setUp(() {
-      stream = StreamController<MapEvent>();
-      controller = PolygonsController(stream: stream);
+      events = StreamController<MapEvent>();
+      controller = PolygonsController(stream: events);
+      controller.bindToMap(123, map);
     });
 
     testWidgets('addPolygons', (WidgetTester tester) async {
@@ -137,7 +144,7 @@
       };
       controller.addPolygons(polygons);
 
-      expect(controller.polygons[PolygonId('1')].polygon.visible, isTrue);
+      expect(controller.polygons[PolygonId('1')]?.polygon?.visible, isTrue);
 
       // Update the polygon
       final updatedPolygons = {
@@ -146,7 +153,7 @@
       controller.changePolygons(updatedPolygons);
 
       expect(controller.polygons.length, 1);
-      expect(controller.polygons[PolygonId('1')].polygon.visible, isFalse);
+      expect(controller.polygons[PolygonId('1')]?.polygon?.visible, isFalse);
     });
 
     testWidgets('removePolygons', (WidgetTester tester) async {
@@ -185,7 +192,7 @@
 
       controller.addPolygons(polygons);
 
-      final polygon = controller.polygons.values.first.polygon;
+      final polygon = controller.polygons.values.first.polygon!;
 
       expect(polygon.get('fillColor'), '#fabada');
       expect(polygon.get('fillOpacity'), closeTo(0.5, _acceptableDelta));
@@ -243,7 +250,7 @@
       final polygon = controller.polygons.values.first.polygon;
       final pointInHole = gmaps.LatLng(28.632, -68.401);
 
-      expect(geometry.poly.containsLocation(pointInHole, polygon), false);
+      expect(geometry.Poly.containsLocation(pointInHole, polygon), false);
     });
 
     testWidgets('Hole Path gets reversed to display correctly',
@@ -268,25 +275,22 @@
 
       controller.addPolygons(polygons);
 
-      expect(
-          controller.polygons.values.first.polygon.paths.getAt(1).getAt(0).lat,
-          28.745);
-      expect(
-          controller.polygons.values.first.polygon.paths.getAt(1).getAt(1).lat,
-          29.57);
-      expect(
-          controller.polygons.values.first.polygon.paths.getAt(1).getAt(2).lat,
-          27.339);
+      final paths = controller.polygons.values.first.polygon!.paths!;
+
+      expect(paths.getAt(1)?.getAt(0)?.lat, 28.745);
+      expect(paths.getAt(1)?.getAt(1)?.lat, 29.57);
+      expect(paths.getAt(1)?.getAt(2)?.lat, 27.339);
     });
   });
 
   group('PolylinesController', () {
-    StreamController<MapEvent> stream;
-    PolylinesController controller;
+    late StreamController<MapEvent> events;
+    late PolylinesController controller;
 
     setUp(() {
-      stream = StreamController<MapEvent>();
-      controller = PolylinesController(stream: stream);
+      events = StreamController<MapEvent>();
+      controller = PolylinesController(stream: events);
+      controller.bindToMap(123, map);
     });
 
     testWidgets('addPolylines', (WidgetTester tester) async {
@@ -309,7 +313,7 @@
       };
       controller.addPolylines(polylines);
 
-      expect(controller.lines[PolylineId('1')].line.visible, isTrue);
+      expect(controller.lines[PolylineId('1')]?.line?.visible, isTrue);
 
       final updatedPolylines = {
         Polyline(polylineId: PolylineId('1'), visible: false),
@@ -317,7 +321,7 @@
       controller.changePolylines(updatedPolylines);
 
       expect(controller.lines.length, 1);
-      expect(controller.lines[PolylineId('1')].line.visible, isFalse);
+      expect(controller.lines[PolylineId('1')]?.line?.visible, isFalse);
     });
 
     testWidgets('removePolylines', (WidgetTester tester) async {
@@ -355,7 +359,7 @@
 
       controller.addPolylines(lines);
 
-      final line = controller.lines.values.first.line;
+      final line = controller.lines.values.first.line!;
 
       expect(line.get('strokeColor'), '#fabada');
       expect(line.get('strokeOpacity'), closeTo(0.5, _acceptableDelta));
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 2895501..311f05a 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
@@ -1,9 +1,15 @@
 name: google_maps_flutter_web_integration_tests
 publish_to: none
 
+# Tests require flutter beta or greater to run.
 environment:
-  sdk: ">=2.2.2 <3.0.0" # Bump this to 2.12 when migrating to nnbd.
-  flutter: ">=1.27.0-0" # For integration_test from sdk
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=2.0.0" 
+  # Actually, Flutter 2.0.0 shipped with a slightly older version of integration_test that
+  # did *not* support null safety. This flutter constraint should be changed to >=2.1.0 when
+  # that version (or greater) is shipped to the `stable` channel.
+  # This causes integration tests to *not* work in `stable`, for now. Please, run integration tests
+  # in `beta` or newer (flutter/plugins runs these tests in `master`).
 
 dependencies:
   google_maps_flutter_web:
@@ -12,12 +18,19 @@
     sdk: flutter
 
 dev_dependencies:
-  google_maps: ^3.4.4
+  build_runner: ^1.11.0
+  google_maps: ^5.1.0
   http: ^0.13.0
-  mockito: ^4.1.1+1 # Update to ^5.0.0 as soon as this migrates to null-safety
+  mockito: ^5.0.0
   flutter_driver:
     sdk: flutter
   flutter_test:
     sdk: flutter
   integration_test:
     sdk: flutter
+
+# Remove these once environment flutter is set to ">=2.1.0".
+# Used to reconcile mockito ^5.0.0 with flutter 2.0.x (current stable).
+dependency_overrides:
+  crypto: ^3.0.0
+  convert: ^3.0.0
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh b/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh
new file mode 100755
index 0000000..78bcdc0
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/bash
+# 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.
+
+flutter pub get
+
+echo "(Re)generating mocks."
+
+flutter pub run build_runner build --delete-conflicting-outputs
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/run_test.sh b/packages/google_maps_flutter/google_maps_flutter_web/example/run_test.sh
index aa52974..fcac5f6 100755
--- a/packages/google_maps_flutter/google_maps_flutter_web/example/run_test.sh
+++ b/packages/google_maps_flutter/google_maps_flutter_web/example/run_test.sh
@@ -6,6 +6,8 @@
 if pgrep -lf chromedriver > /dev/null; then
   echo "chromedriver is running."
 
+  ./regen_mocks.sh
+
   if [ $# -eq 0 ]; then
     echo "No target specified, running all tests..."
     find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}'
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 f324d1c..3121d18 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
@@ -12,4 +12,3 @@
         <script src="main.dart.js"></script>
     </body>
 </html>
-
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 0aa563c..6dc2dab 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
@@ -6,6 +6,7 @@
 
 import 'dart:async';
 import 'dart:html';
+import 'dart:js_util';
 import 'src/shims/dart_ui.dart' as ui; // Conditionally imports dart:ui in web
 import 'dart:convert';
 
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart
index 84bae1b..65057d8 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart
@@ -6,15 +6,15 @@
 
 /// The `CircleController` class wraps a [gmaps.Circle] and its `onTap` behavior.
 class CircleController {
-  gmaps.Circle _circle;
+  gmaps.Circle? _circle;
 
   final bool _consumeTapEvents;
 
   /// Creates a `CircleController`, which wraps a [gmaps.Circle] object and its `onTap` behavior.
   CircleController({
-    @required gmaps.Circle circle,
+    required gmaps.Circle circle,
     bool consumeTapEvents = false,
-    ui.VoidCallback onTap,
+    ui.VoidCallback? onTap,
   })  : _circle = circle,
         _consumeTapEvents = consumeTapEvents {
     if (onTap != null) {
@@ -26,21 +26,26 @@
 
   /// Returns the wrapped [gmaps.Circle]. Only used for testing.
   @visibleForTesting
-  gmaps.Circle get circle => _circle;
+  gmaps.Circle? get circle => _circle;
 
   /// Returns `true` if this Controller will use its own `onTap` handler to consume events.
   bool get consumeTapEvents => _consumeTapEvents;
 
   /// Updates the options of the wrapped [gmaps.Circle] object.
+  ///
+  /// This cannot be called after [remove].
   void update(gmaps.CircleOptions options) {
-    _circle.options = options;
+    assert(_circle != null, 'Cannot `update` Circle after calling `remove`.');
+    _circle!.options = options;
   }
 
   /// Disposes of the currently wrapped [gmaps.Circle].
   void remove() {
-    _circle.visible = false;
-    _circle.radius = 0;
-    _circle.map = null;
-    _circle = null;
+    if (_circle != null) {
+      _circle!.visible = false;
+      _circle!.radius = 0;
+      _circle!.map = null;
+      _circle = null;
+    }
   }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart
index 659d8ac..2a19d87 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart
@@ -14,8 +14,8 @@
 
   /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers.
   CirclesController({
-    @required StreamController<MapEvent> stream,
-  })  : _streamController = stream,
+    required StreamController<MapEvent> stream,
+  })   : _streamController = stream,
         _circleIdToController = Map<CircleId, CircleController>();
 
   /// Returns the cache of [CircleController]s. Test only.
@@ -26,7 +26,7 @@
   ///
   /// Wraps each [Circle] into its corresponding [CircleController].
   void addCircles(Set<Circle> circlesToAdd) {
-    circlesToAdd?.forEach((circle) {
+    circlesToAdd.forEach((circle) {
       _addCircle(circle);
     });
   }
@@ -50,20 +50,21 @@
 
   /// Updates a set of [Circle] objects with new options.
   void changeCircles(Set<Circle> circlesToChange) {
-    circlesToChange?.forEach((circleToChange) {
+    circlesToChange.forEach((circleToChange) {
       _changeCircle(circleToChange);
     });
   }
 
   void _changeCircle(Circle circle) {
-    final circleController = _circleIdToController[circle?.circleId];
+    final circleController = _circleIdToController[circle.circleId];
     circleController?.update(_circleOptionsFromCircle(circle));
   }
 
   /// Removes a set of [CircleId]s from the cache.
   void removeCircles(Set<CircleId> circleIdsToRemove) {
-    circleIdsToRemove?.forEach((circleId) {
-      final CircleController circleController = _circleIdToController[circleId];
+    circleIdsToRemove.forEach((circleId) {
+      final CircleController? circleController =
+          _circleIdToController[circleId];
       circleController?.remove();
       _circleIdToController.remove(circleId);
     });
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 c875bf7..2e71c79 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
@@ -4,11 +4,10 @@
 
 part of google_maps_flutter_web;
 
-final _nullLatLng = LatLng(0, 0);
-final _nullLatLngBounds = LatLngBounds(
-  northeast: _nullLatLng,
-  southwest: _nullLatLng,
-);
+// Default values for when the gmaps objects return null/undefined values.
+final _nullGmapsLatLng = gmaps.LatLng(0, 0);
+final _nullGmapsLatLngBounds =
+    gmaps.LatLngBounds(_nullGmapsLatLng, _nullGmapsLatLng);
 
 // Defaults taken from the Google Maps Platform SDK documentation.
 final _defaultCssColor = '#000000';
@@ -115,80 +114,6 @@
   return rawOptions['trafficEnabled'] ?? false;
 }
 
-// Coverts the incoming JSON object into a List of MapTypeStyler objects.
-List<gmaps.MapTypeStyler> _parseStylers(List stylerJsons) {
-  return stylerJsons?.map((styler) {
-    return gmaps.MapTypeStyler()
-      ..color = styler['color']
-      ..gamma = styler['gamma']
-      ..hue = styler['hue']
-      ..invertLightness = styler['invertLightness']
-      ..lightness = styler['lightness']
-      ..saturation = styler['saturation']
-      ..visibility = styler['visibility']
-      ..weight = styler['weight'];
-  })?.toList();
-}
-
-// Converts a String to its corresponding MapTypeStyleElementType enum value.
-final _elementTypeToEnum = <String, gmaps.MapTypeStyleElementType>{
-  'all': gmaps.MapTypeStyleElementType.ALL,
-  'geometry': gmaps.MapTypeStyleElementType.GEOMETRY,
-  'geometry.fill': gmaps.MapTypeStyleElementType.GEOMETRY_FILL,
-  'geometry.stroke': gmaps.MapTypeStyleElementType.GEOMETRY_STROKE,
-  'labels': gmaps.MapTypeStyleElementType.LABELS,
-  'labels.icon': gmaps.MapTypeStyleElementType.LABELS_ICON,
-  'labels.text': gmaps.MapTypeStyleElementType.LABELS_TEXT,
-  'labels.text.fill': gmaps.MapTypeStyleElementType.LABELS_TEXT_FILL,
-  'labels.text.stroke': gmaps.MapTypeStyleElementType.LABELS_TEXT_STROKE,
-};
-
-// Converts a String to its corresponding MapTypeStyleFeatureType enum value.
-final _featureTypeToEnum = <String, gmaps.MapTypeStyleFeatureType>{
-  'administrative': gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE,
-  'administrative.country':
-      gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_COUNTRY,
-  'administrative.land_parcel':
-      gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_LAND_PARCEL,
-  'administrative.locality':
-      gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_LOCALITY,
-  'administrative.neighborhood':
-      gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_NEIGHBORHOOD,
-  'administrative.province':
-      gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_PROVINCE,
-  'all': gmaps.MapTypeStyleFeatureType.ALL,
-  'landscape': gmaps.MapTypeStyleFeatureType.LANDSCAPE,
-  'landscape.man_made': gmaps.MapTypeStyleFeatureType.LANDSCAPE_MAN_MADE,
-  'landscape.natural': gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL,
-  'landscape.natural.landcover':
-      gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL_LANDCOVER,
-  'landscape.natural.terrain':
-      gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL_TERRAIN,
-  'poi': gmaps.MapTypeStyleFeatureType.POI,
-  'poi.attraction': gmaps.MapTypeStyleFeatureType.POI_ATTRACTION,
-  'poi.business': gmaps.MapTypeStyleFeatureType.POI_BUSINESS,
-  'poi.government': gmaps.MapTypeStyleFeatureType.POI_GOVERNMENT,
-  'poi.medical': gmaps.MapTypeStyleFeatureType.POI_MEDICAL,
-  'poi.park': gmaps.MapTypeStyleFeatureType.POI_PARK,
-  'poi.place_of_worship': gmaps.MapTypeStyleFeatureType.POI_PLACE_OF_WORSHIP,
-  'poi.school': gmaps.MapTypeStyleFeatureType.POI_SCHOOL,
-  'poi.sports_complex': gmaps.MapTypeStyleFeatureType.POI_SPORTS_COMPLEX,
-  'road': gmaps.MapTypeStyleFeatureType.ROAD,
-  'road.arterial': gmaps.MapTypeStyleFeatureType.ROAD_ARTERIAL,
-  'road.highway': gmaps.MapTypeStyleFeatureType.ROAD_HIGHWAY,
-  'road.highway.controlled_access':
-      gmaps.MapTypeStyleFeatureType.ROAD_HIGHWAY_CONTROLLED_ACCESS,
-  'road.local': gmaps.MapTypeStyleFeatureType.ROAD_LOCAL,
-  'transit': gmaps.MapTypeStyleFeatureType.TRANSIT,
-  'transit.line': gmaps.MapTypeStyleFeatureType.TRANSIT_LINE,
-  'transit.station': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION,
-  'transit.station.airport':
-      gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_AIRPORT,
-  'transit.station.bus': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_BUS,
-  'transit.station.rail': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_RAIL,
-  'water': gmaps.MapTypeStyleFeatureType.WATER,
-};
-
 // The keys we'd expect to see in a serialized MapTypeStyle JSON object.
 final _mapStyleKeys = {
   'elementType',
@@ -202,37 +127,36 @@
 }
 
 // Converts an incoming JSON-encoded Style info, into the correct gmaps array.
-List<gmaps.MapTypeStyle> _mapStyles(String mapStyleJson) {
+List<gmaps.MapTypeStyle> _mapStyles(String? mapStyleJson) {
   List<gmaps.MapTypeStyle> styles = [];
   if (mapStyleJson != null) {
-    styles = json.decode(mapStyleJson, reviver: (key, value) {
-      if (value is Map && _isJsonMapStyle(value)) {
-        return gmaps.MapTypeStyle()
-          ..elementType = _elementTypeToEnum[value['elementType']]
-          ..featureType = _featureTypeToEnum[value['featureType']]
-          ..stylers = _parseStylers(value['stylers']);
-      }
-      return value;
-    }).cast<gmaps.MapTypeStyle>();
+    styles = json
+        .decode(mapStyleJson, reviver: (key, value) {
+          if (value is Map && _isJsonMapStyle(value)) {
+            return gmaps.MapTypeStyle()
+              ..elementType = value['elementType']
+              ..featureType = value['featureType']
+              ..stylers =
+                  (value['stylers'] as List).map((e) => jsify(e)).toList();
+          }
+          return value;
+        })
+        .cast<gmaps.MapTypeStyle>()
+        .toList();
+    // .toList calls are required so the JS API understands the underlying data structure.
   }
   return styles;
 }
 
 gmaps.LatLng _latLngToGmLatLng(LatLng latLng) {
-  if (latLng == null) return null;
   return gmaps.LatLng(latLng.latitude, latLng.longitude);
 }
 
 LatLng _gmLatLngToLatLng(gmaps.LatLng latLng) {
-  if (latLng == null) return _nullLatLng;
-  return LatLng(latLng.lat, latLng.lng);
+  return LatLng(latLng.lat.toDouble(), latLng.lng.toDouble());
 }
 
 LatLngBounds _gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) {
-  if (latLngBounds == null) {
-    return _nullLatLngBounds;
-  }
-
   return LatLngBounds(
     southwest: _gmLatLngToLatLng(latLngBounds.southWest),
     northeast: _gmLatLngToLatLng(latLngBounds.northEast),
@@ -241,10 +165,10 @@
 
 CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) {
   return CameraPosition(
-    target: _gmLatLngToLatLng(map.center),
-    bearing: map.heading ?? 0,
-    tilt: map.tilt ?? 0,
-    zoom: map.zoom?.toDouble() ?? 10,
+    target: _gmLatLngToLatLng(map.center ?? _nullGmapsLatLng),
+    bearing: map.heading?.toDouble() ?? 0,
+    tilt: map.tilt?.toDouble() ?? 0,
+    zoom: map.zoom?.toDouble() ?? 0,
   );
 }
 
@@ -252,9 +176,13 @@
 // TODO: Move to their appropriate objects, maybe make these copy constructors:
 // Marker.fromMarker(anotherMarker, moreOptions);
 
-gmaps.InfoWindowOptions _infoWindowOptionsFromMarker(Marker marker) {
-  if ((marker.infoWindow?.title?.isEmpty ?? true) &&
-      (marker.infoWindow?.snippet?.isEmpty ?? true)) {
+gmaps.InfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) {
+  final markerTitle = marker.infoWindow.title ?? '';
+  final markerSnippet = marker.infoWindow.snippet ?? '';
+
+  // If both the title and snippet of an infowindow are empty, we don't really
+  // want an infowindow...
+  if ((markerTitle.isEmpty) && (markerSnippet.isEmpty)) {
     return null;
   }
 
@@ -262,17 +190,18 @@
   // to click events...
   final HtmlElement container = DivElement()
     ..id = 'gmaps-marker-${marker.markerId.value}-infowindow';
-  if (marker.infoWindow.title?.isNotEmpty ?? false) {
+
+  if (markerTitle.isNotEmpty) {
     final HtmlElement title = HeadingElement.h3()
       ..className = 'infowindow-title'
-      ..innerText = marker.infoWindow.title;
+      ..innerText = markerTitle;
     container.children.add(title);
   }
-  if (marker.infoWindow.snippet?.isNotEmpty ?? false) {
+  if (markerSnippet.isNotEmpty) {
     final HtmlElement snippet = DivElement()
       ..className = 'infowindow-snippet'
       ..setInnerHtml(
-        sanitizeHtml(marker.infoWindow.snippet),
+        sanitizeHtml(markerSnippet),
         treeSanitizer: NodeTreeSanitizer.trusted,
       );
     container.children.add(snippet);
@@ -290,10 +219,10 @@
 // Preserves the position from the [currentMarker], if set.
 gmaps.MarkerOptions _markerOptionsFromMarker(
   Marker marker,
-  gmaps.Marker currentMarker,
+  gmaps.Marker? currentMarker,
 ) {
-  final iconConfig = marker.icon?.toJson() as List;
-  gmaps.Icon icon;
+  final iconConfig = marker.icon.toJson() as List;
+  gmaps.Icon? icon;
 
   if (iconConfig != null) {
     if (iconConfig[0] == 'fromAssetImage') {
@@ -324,7 +253,7 @@
           marker.position.latitude,
           marker.position.longitude,
         )
-    ..title = sanitizeHtml(marker.infoWindow?.title ?? "")
+    ..title = sanitizeHtml(marker.infoWindow.title ?? "")
     ..zIndex = marker.zIndex
     ..visible = marker.visible
     ..opacity = marker.alpha
@@ -356,7 +285,7 @@
   final polygonDirection = _isPolygonClockwise(path);
   List<List<gmaps.LatLng>> paths = [path];
   int holeIndex = 0;
-  polygon.holes?.forEach((hole) {
+  polygon.holes.forEach((hole) {
     List<gmaps.LatLng> holePath =
         hole.map((point) => _latLngToGmLatLng(point)).toList();
     if (_isPolygonClockwise(holePath) == polygonDirection) {
@@ -387,6 +316,13 @@
 /// based on: https://stackoverflow.com/a/1165943
 ///
 /// returns [true] if clockwise [false] if counterclockwise
+///
+/// This method expects that the incoming [path] is a `List` of well-formed,
+/// non-null [gmaps.LatLng] objects.
+///
+/// Currently, this method is only called from [_polygonOptionsFromPolygon], and
+/// the `path` is a transformed version of [Polygon.points] or each of the
+/// [Polygon.holes], guaranteeing that `lat` and `lng` can be accessed with `!`.
 bool _isPolygonClockwise(List<gmaps.LatLng> path) {
   var direction = 0.0;
   for (var i = 0; i < path.length; i++) {
@@ -447,7 +383,7 @@
       map.panBy(json[1], json[2]);
       break;
     case 'zoomBy':
-      gmaps.LatLng focusLatLng;
+      gmaps.LatLng? focusLatLng;
       double zoomDelta = json[1] ?? 0;
       // Web only supports integer changes...
       int newZoomDelta = zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil();
@@ -460,16 +396,16 @@
           // print('Error computing new focus LatLng. JS Error: ' + e.toString());
         }
       }
-      map.zoom = map.zoom + newZoomDelta;
+      map.zoom = (map.zoom ?? 0) + newZoomDelta;
       if (focusLatLng != null) {
         map.panTo(focusLatLng);
       }
       break;
     case 'zoomIn':
-      map.zoom++;
+      map.zoom = (map.zoom ?? 0) + 1;
       break;
     case 'zoomOut':
-      map.zoom--;
+      map.zoom = (map.zoom ?? 0) - 1;
       break;
     case 'zoomTo':
       map.zoom = json[1];
@@ -481,17 +417,27 @@
 
 // original JS by: Byron Singh (https://stackoverflow.com/a/30541162)
 gmaps.LatLng _pixelToLatLng(gmaps.GMap map, int x, int y) {
-  final ne = map.bounds.northEast;
-  final sw = map.bounds.southWest;
+  final bounds = map.bounds;
   final projection = map.projection;
+  final zoom = map.zoom;
 
-  final topRight = projection.fromLatLngToPoint(ne);
-  final bottomLeft = projection.fromLatLngToPoint(sw);
+  assert(
+      bounds != null, 'Map Bounds required to compute LatLng of screen x/y.');
+  assert(projection != null,
+      'Map Projection required to compute LatLng of screen x/y');
+  assert(zoom != null,
+      'Current map zoom level required to compute LatLng of screen x/y');
 
-  final scale = 1 << map.zoom; // 2 ^ zoom
+  final ne = bounds!.northEast;
+  final sw = bounds.southWest;
+
+  final topRight = projection!.fromLatLngToPoint!(ne)!;
+  final bottomLeft = projection.fromLatLngToPoint!(sw)!;
+
+  final scale = 1 << (zoom!.toInt()); // 2 ^ zoom
 
   final point =
-      gmaps.Point((x / scale) + bottomLeft.x, (y / scale) + topRight.y);
+      gmaps.Point((x / scale) + bottomLeft.x!, (y / scale) + topRight.y!);
 
-  return projection.fromPointToLatLng(point);
+  return projection.fromPointToLatLng!(point)!;
 }
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 cc8d79a..2528490 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
@@ -27,11 +27,11 @@
   String _getViewType(int mapId) => 'plugins.flutter.io/google_maps_$mapId';
 
   // The Flutter widget that contains the rendered Map.
-  HtmlElementView _widget;
-  HtmlElement _div;
+  HtmlElementView? _widget;
+  late HtmlElement _div;
 
   /// The Flutter widget that will contain the rendered Map. Used for caching.
-  HtmlElementView get widget {
+  Widget? get widget {
     if (_widget == null && !_streamController.isClosed) {
       _widget = HtmlElementView(
         viewType: _getViewType(_mapId),
@@ -41,14 +41,14 @@
   }
 
   // The currently-enabled traffic layer.
-  gmaps.TrafficLayer _trafficLayer;
+  gmaps.TrafficLayer? _trafficLayer;
 
   /// A getter for the current traffic layer. Only for tests.
   @visibleForTesting
-  gmaps.TrafficLayer get trafficLayer => _trafficLayer;
+  gmaps.TrafficLayer? get trafficLayer => _trafficLayer;
 
   // The underlying GMap instance. This is the interface with the JS SDK.
-  gmaps.GMap _googleMap;
+  gmaps.GMap? _googleMap;
 
   // The StreamController used by this controller and the geometry ones.
   final StreamController<MapEvent> _streamController;
@@ -57,10 +57,10 @@
   Stream<MapEvent> get events => _streamController.stream;
 
   // Geometry controllers, for different features of the map.
-  CirclesController _circlesController;
-  PolygonsController _polygonsController;
-  PolylinesController _polylinesController;
-  MarkersController _markersController;
+  CirclesController? _circlesController;
+  PolygonsController? _polygonsController;
+  PolylinesController? _polylinesController;
+  MarkersController? _markersController;
   // Keeps track if _attachGeometryControllers has been called or not.
   bool _controllersBoundToMap = false;
 
@@ -69,9 +69,9 @@
 
   /// Initializes the GMap, and the sub-controllers related to it. Wires events.
   GoogleMapController({
-    @required int mapId,
-    @required StreamController<MapEvent> streamController,
-    @required CameraPosition initialCameraPosition,
+    required int mapId,
+    required StreamController<MapEvent> streamController,
+    required CameraPosition initialCameraPosition,
     Set<Marker> markers = const <Marker>{},
     Set<Polygon> polygons = const <Polygon>{},
     Set<Polyline> polylines = const <Polyline>{},
@@ -107,11 +107,11 @@
   /// Overrides certain properties to install mocks defined during testing.
   @visibleForTesting
   void debugSetOverrides({
-    DebugCreateMapFunction createMap,
-    MarkersController markers,
-    CirclesController circles,
-    PolygonsController polygons,
-    PolylinesController polylines,
+    DebugCreateMapFunction? createMap,
+    MarkersController? markers,
+    CirclesController? circles,
+    PolygonsController? polygons,
+    PolylinesController? polylines,
   }) {
     _overrideCreateMap = createMap;
     _markersController = markers ?? _markersController;
@@ -120,11 +120,11 @@
     _polylinesController = polylines ?? _polylinesController;
   }
 
-  DebugCreateMapFunction _overrideCreateMap;
+  DebugCreateMapFunction? _overrideCreateMap;
 
   gmaps.GMap _createMap(HtmlElement div, gmaps.MapOptions options) {
     if (_overrideCreateMap != null) {
-      return _overrideCreateMap(div, options);
+      return _overrideCreateMap!(div, options);
     }
     return gmaps.GMap(div, options);
   }
@@ -142,10 +142,11 @@
     options = _applyInitialPosition(_initialCameraPosition, options);
 
     // Create the map...
-    _googleMap = _createMap(_div, options);
+    final map = _createMap(_div, options);
+    _googleMap = map;
 
-    _attachMapEvents(_googleMap);
-    _attachGeometryControllers(_googleMap);
+    _attachMapEvents(map);
+    _attachGeometryControllers(map);
 
     _renderInitialGeometry(
       markers: _markers,
@@ -154,19 +155,21 @@
       polylines: _polylines,
     );
 
-    _setTrafficLayer(_googleMap, _isTrafficLayerEnabled(_rawMapOptions));
+    _setTrafficLayer(map, _isTrafficLayerEnabled(_rawMapOptions));
   }
 
   // Funnels map gmap events into the plugin's stream controller.
   void _attachMapEvents(gmaps.GMap map) {
     map.onClick.listen((event) {
+      assert(event.latLng != null);
       _streamController.add(
-        MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng)),
+        MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
       );
     });
     map.onRightclick.listen((event) {
+      assert(event.latLng != null);
       _streamController.add(
-        MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng)),
+        MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
       );
     });
     map.onBoundsChanged.listen((event) {
@@ -188,28 +191,47 @@
   void _attachGeometryControllers(gmaps.GMap map) {
     // Now we can add the initial geometry.
     // And bind the (ready) map instance to the other geometry controllers.
-    _circlesController.bindToMap(_mapId, map);
-    _polygonsController.bindToMap(_mapId, map);
-    _polylinesController.bindToMap(_mapId, map);
-    _markersController.bindToMap(_mapId, map);
+    //
+    // These controllers are either created in the constructor of this class, or
+    // overriden (for testing) by the [debugSetOverrides] method. They can't be
+    // null.
+    assert(_circlesController != null,
+        'Cannot attach a map to a null CirclesController instance.');
+    assert(_polygonsController != null,
+        'Cannot attach a map to a null PolygonsController instance.');
+    assert(_polylinesController != null,
+        'Cannot attach a map to a null PolylinesController instance.');
+    assert(_markersController != null,
+        'Cannot attach a map to a null MarkersController instance.');
+
+    _circlesController!.bindToMap(_mapId, map);
+    _polygonsController!.bindToMap(_mapId, map);
+    _polylinesController!.bindToMap(_mapId, map);
+    _markersController!.bindToMap(_mapId, map);
+
     _controllersBoundToMap = true;
   }
 
   // Renders the initial sets of geometry.
   void _renderInitialGeometry({
-    Set<Marker> markers,
-    Set<Circle> circles,
-    Set<Polygon> polygons,
-    Set<Polyline> polylines,
+    Set<Marker> markers = const {},
+    Set<Circle> circles = const {},
+    Set<Polygon> polygons = const {},
+    Set<Polyline> polylines = const {},
   }) {
     assert(
         _controllersBoundToMap,
         'Geometry controllers must be bound to a map before any geometry can ' +
             'be added to them. Ensure _attachGeometryControllers is called first.');
-    _markersController.addMarkers(markers);
-    _circlesController.addCircles(circles);
-    _polygonsController.addPolygons(polygons);
-    _polylinesController.addPolylines(polylines);
+
+    // The above assert will only succeed if the controllers have been bound to a map
+    // 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);
   }
 
   // Merges new options coming from the plugin into the _rawMapOptions map.
@@ -227,10 +249,12 @@
   ///
   /// This method converts the map into the proper [gmaps.MapOptions]
   void updateRawOptions(Map<String, dynamic> optionsUpdate) {
+    assert(_googleMap != null, 'Cannot update options on a null map.');
+
     final newOptions = _mergeRawOptions(optionsUpdate);
 
     _setOptions(_rawOptionsToGmapsOptions(newOptions));
-    _setTrafficLayer(_googleMap, _isTrafficLayerEnabled(newOptions));
+    _setTrafficLayer(_googleMap!, _isTrafficLayerEnabled(newOptions));
   }
 
   // Sets new [gmaps.MapOptions] on the wrapped map.
@@ -241,11 +265,10 @@
   // Attaches/detaches a Traffic Layer on the passed `map` if `attach` is true/false.
   void _setTrafficLayer(gmaps.GMap map, bool attach) {
     if (attach && _trafficLayer == null) {
-      _trafficLayer = gmaps.TrafficLayer();
-      _trafficLayer.set('map', map);
+      _trafficLayer = gmaps.TrafficLayer()..set('map', map);
     }
     if (!attach && _trafficLayer != null) {
-      _trafficLayer.set('map', null);
+      _trafficLayer!.set('map', null);
       _trafficLayer = null;
     }
   }
@@ -255,35 +278,61 @@
 
   /// Returns the [LatLngBounds] of the current viewport.
   Future<LatLngBounds> getVisibleRegion() async {
-    return _gmLatLngBoundsTolatLngBounds(await _googleMap.bounds);
+    assert(_googleMap != null, 'Cannot get the visible region of a null map.');
+
+    return _gmLatLngBoundsTolatLngBounds(
+      await _googleMap!.bounds ?? _nullGmapsLatLngBounds,
+    );
   }
 
   /// Returns the [ScreenCoordinate] for a given viewport [LatLng].
   Future<ScreenCoordinate> getScreenCoordinate(LatLng latLng) async {
+    assert(_googleMap != null,
+        'Cannot get the screen coordinates with a null map.');
+    assert(_googleMap!.projection != null,
+        'Cannot compute screen coordinate with a null map or projection.');
+
     final point =
-        _googleMap.projection.fromLatLngToPoint(_latLngToGmLatLng(latLng));
-    return ScreenCoordinate(x: point.x, y: point.y);
+        _googleMap!.projection!.fromLatLngToPoint!(_latLngToGmLatLng(latLng))!;
+
+    assert(point.x != null && point.y != null,
+        'The x and y of a ScreenCoordinate cannot be null.');
+
+    return ScreenCoordinate(x: point.x!.toInt(), y: point.y!.toInt());
   }
 
   /// Returns the [LatLng] for a `screenCoordinate` (in pixels) of the viewport.
   Future<LatLng> getLatLng(ScreenCoordinate screenCoordinate) async {
+    assert(_googleMap != null,
+        'Cannot get the lat, lng of a screen coordinate with a null map.');
+
     final gmaps.LatLng latLng =
-        _pixelToLatLng(_googleMap, screenCoordinate.x, screenCoordinate.y);
+        _pixelToLatLng(_googleMap!, screenCoordinate.x, screenCoordinate.y);
     return _gmLatLngToLatLng(latLng);
   }
 
   /// Applies a `cameraUpdate` to the current viewport.
   Future<void> moveCamera(CameraUpdate cameraUpdate) async {
-    return _applyCameraUpdate(_googleMap, cameraUpdate);
+    assert(_googleMap != null, 'Cannot update the camera of a null map.');
+
+    return _applyCameraUpdate(_googleMap!, cameraUpdate);
   }
 
   /// Returns the zoom level of the current viewport.
-  Future<double> getZoomLevel() async => _googleMap.zoom.toDouble();
+  Future<double> getZoomLevel() async {
+    assert(_googleMap != null, 'Cannot get zoom level of a null map.');
+    assert(_googleMap!.zoom != null,
+        'Zoom level should not be null. Is the map correctly initialized?');
+
+    return _googleMap!.zoom!.toDouble();
+  }
 
   // Geometry manipulation
 
   /// Applies [CircleUpdates] to the currently managed circles.
   void updateCircles(CircleUpdates updates) {
+    assert(
+        _circlesController != null, 'Cannot update circles after dispose().');
     _circlesController?.addCircles(updates.circlesToAdd);
     _circlesController?.changeCircles(updates.circlesToChange);
     _circlesController?.removeCircles(updates.circleIdsToRemove);
@@ -291,6 +340,8 @@
 
   /// Applies [PolygonUpdates] to the currently managed polygons.
   void updatePolygons(PolygonUpdates updates) {
+    assert(
+        _polygonsController != null, 'Cannot update polygons after dispose().');
     _polygonsController?.addPolygons(updates.polygonsToAdd);
     _polygonsController?.changePolygons(updates.polygonsToChange);
     _polygonsController?.removePolygons(updates.polygonIdsToRemove);
@@ -298,6 +349,8 @@
 
   /// Applies [PolylineUpdates] to the currently managed lines.
   void updatePolylines(PolylineUpdates updates) {
+    assert(_polylinesController != null,
+        'Cannot update polylines after dispose().');
     _polylinesController?.addPolylines(updates.polylinesToAdd);
     _polylinesController?.changePolylines(updates.polylinesToChange);
     _polylinesController?.removePolylines(updates.polylineIdsToRemove);
@@ -305,6 +358,8 @@
 
   /// Applies [MarkerUpdates] to the currently managed markers.
   void updateMarkers(MarkerUpdates updates) {
+    assert(
+        _markersController != null, 'Cannot update markers after dispose().');
     _markersController?.addMarkers(updates.markersToAdd);
     _markersController?.changeMarkers(updates.markersToChange);
     _markersController?.removeMarkers(updates.markerIdsToRemove);
@@ -312,22 +367,29 @@
 
   /// Shows the [InfoWindow] of the marker identified by its [MarkerId].
   void showInfoWindow(MarkerId markerId) {
+    assert(_markersController != null,
+        'Cannot show infowindow of marker [${markerId.value}] after dispose().');
     _markersController?.showMarkerInfoWindow(markerId);
   }
 
   /// Hides the [InfoWindow] of the marker identified by its [MarkerId].
   void hideInfoWindow(MarkerId markerId) {
+    assert(_markersController != null,
+        'Cannot hide infowindow of marker [${markerId.value}] after dispose().');
     _markersController?.hideMarkerInfoWindow(markerId);
   }
 
   /// Returns true if the [InfoWindow] of the marker identified by [MarkerId] is shown.
   bool isInfoWindowShown(MarkerId markerId) {
-    return _markersController?.isInfoWindowShown(markerId);
+    return _markersController?.isInfoWindowShown(markerId) ?? false;
   }
 
   // Cleanup
 
   /// Disposes of this controller and its resources.
+  ///
+  /// You won't be able to call many of the methods on this controller after
+  /// calling `dispose`!
   void dispose() {
     _widget = null;
     _googleMap = null;
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 5e95a53..692917f 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
@@ -45,7 +45,7 @@
   @override
   Future<void> updateMapOptions(
     Map<String, dynamic> optionsUpdate, {
-    @required int mapId,
+    required int mapId,
   }) async {
     _map(mapId).updateRawOptions(optionsUpdate);
   }
@@ -54,7 +54,7 @@
   @override
   Future<void> updateMarkers(
     MarkerUpdates markerUpdates, {
-    @required int mapId,
+    required int mapId,
   }) async {
     _map(mapId).updateMarkers(markerUpdates);
   }
@@ -63,7 +63,7 @@
   @override
   Future<void> updatePolygons(
     PolygonUpdates polygonUpdates, {
-    @required int mapId,
+    required int mapId,
   }) async {
     _map(mapId).updatePolygons(polygonUpdates);
   }
@@ -72,7 +72,7 @@
   @override
   Future<void> updatePolylines(
     PolylineUpdates polylineUpdates, {
-    @required int mapId,
+    required int mapId,
   }) async {
     _map(mapId).updatePolylines(polylineUpdates);
   }
@@ -81,15 +81,15 @@
   @override
   Future<void> updateCircles(
     CircleUpdates circleUpdates, {
-    @required int mapId,
+    required int mapId,
   }) async {
     _map(mapId).updateCircles(circleUpdates);
   }
 
   @override
   Future<void> updateTileOverlays({
-    @required Set<TileOverlay> newTileOverlays,
-    @required int mapId,
+    required Set<TileOverlay> newTileOverlays,
+    required int mapId,
   }) async {
     return; // Noop for now!
   }
@@ -97,7 +97,7 @@
   @override
   Future<void> clearTileCache(
     TileOverlayId tileOverlayId, {
-    @required int mapId,
+    required int mapId,
   }) async {
     return; // Noop for now!
   }
@@ -106,7 +106,7 @@
   @override
   Future<void> animateCamera(
     CameraUpdate cameraUpdate, {
-    @required int mapId,
+    required int mapId,
   }) async {
     return moveCamera(cameraUpdate, mapId: mapId);
   }
@@ -115,7 +115,7 @@
   @override
   Future<void> moveCamera(
     CameraUpdate cameraUpdate, {
-    @required int mapId,
+    required int mapId,
   }) async {
     return _map(mapId).moveCamera(cameraUpdate);
   }
@@ -128,8 +128,8 @@
   /// pass full styles.
   @override
   Future<void> setMapStyle(
-    String mapStyle, {
-    @required int mapId,
+    String? mapStyle, {
+    required int mapId,
   }) async {
     _map(mapId).updateRawOptions({
       'styles': _mapStyles(mapStyle),
@@ -139,7 +139,7 @@
   /// Returns the bounds of the current viewport.
   @override
   Future<LatLngBounds> getVisibleRegion({
-    @required int mapId,
+    required int mapId,
   }) {
     return _map(mapId).getVisibleRegion();
   }
@@ -148,7 +148,7 @@
   @override
   Future<ScreenCoordinate> getScreenCoordinate(
     LatLng latLng, {
-    @required int mapId,
+    required int mapId,
   }) {
     return _map(mapId).getScreenCoordinate(latLng);
   }
@@ -157,7 +157,7 @@
   @override
   Future<LatLng> getLatLng(
     ScreenCoordinate screenCoordinate, {
-    @required int mapId,
+    required int mapId,
   }) {
     return _map(mapId).getLatLng(screenCoordinate);
   }
@@ -170,7 +170,7 @@
   @override
   Future<void> showMarkerInfoWindow(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) async {
     _map(mapId).showInfoWindow(markerId);
   }
@@ -183,7 +183,7 @@
   @override
   Future<void> hideMarkerInfoWindow(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) async {
     _map(mapId).hideInfoWindow(markerId);
   }
@@ -196,7 +196,7 @@
   @override
   Future<bool> isMarkerInfoWindowShown(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) async {
     return _map(mapId).isInfoWindowShown(markerId);
   }
@@ -204,7 +204,7 @@
   /// Returns the zoom level of the `mapId`.
   @override
   Future<double> getZoomLevel({
-    @required int mapId,
+    required int mapId,
   }) {
     return _map(mapId).getZoomLevel();
   }
@@ -213,64 +213,64 @@
   // into the plugin
 
   @override
-  Stream<CameraMoveStartedEvent> onCameraMoveStarted({@required int mapId}) {
+  Stream<CameraMoveStartedEvent> onCameraMoveStarted({required int mapId}) {
     return _events(mapId).whereType<CameraMoveStartedEvent>();
   }
 
   @override
-  Stream<CameraMoveEvent> onCameraMove({@required int mapId}) {
+  Stream<CameraMoveEvent> onCameraMove({required int mapId}) {
     return _events(mapId).whereType<CameraMoveEvent>();
   }
 
   @override
-  Stream<CameraIdleEvent> onCameraIdle({@required int mapId}) {
+  Stream<CameraIdleEvent> onCameraIdle({required int mapId}) {
     return _events(mapId).whereType<CameraIdleEvent>();
   }
 
   @override
-  Stream<MarkerTapEvent> onMarkerTap({@required int mapId}) {
+  Stream<MarkerTapEvent> onMarkerTap({required int mapId}) {
     return _events(mapId).whereType<MarkerTapEvent>();
   }
 
   @override
-  Stream<InfoWindowTapEvent> onInfoWindowTap({@required int mapId}) {
+  Stream<InfoWindowTapEvent> onInfoWindowTap({required int mapId}) {
     return _events(mapId).whereType<InfoWindowTapEvent>();
   }
 
   @override
-  Stream<MarkerDragEndEvent> onMarkerDragEnd({@required int mapId}) {
+  Stream<MarkerDragEndEvent> onMarkerDragEnd({required int mapId}) {
     return _events(mapId).whereType<MarkerDragEndEvent>();
   }
 
   @override
-  Stream<PolylineTapEvent> onPolylineTap({@required int mapId}) {
+  Stream<PolylineTapEvent> onPolylineTap({required int mapId}) {
     return _events(mapId).whereType<PolylineTapEvent>();
   }
 
   @override
-  Stream<PolygonTapEvent> onPolygonTap({@required int mapId}) {
+  Stream<PolygonTapEvent> onPolygonTap({required int mapId}) {
     return _events(mapId).whereType<PolygonTapEvent>();
   }
 
   @override
-  Stream<CircleTapEvent> onCircleTap({@required int mapId}) {
+  Stream<CircleTapEvent> onCircleTap({required int mapId}) {
     return _events(mapId).whereType<CircleTapEvent>();
   }
 
   @override
-  Stream<MapTapEvent> onTap({@required int mapId}) {
+  Stream<MapTapEvent> onTap({required int mapId}) {
     return _events(mapId).whereType<MapTapEvent>();
   }
 
   @override
-  Stream<MapLongPressEvent> onLongPress({@required int mapId}) {
+  Stream<MapLongPressEvent> onLongPress({required int mapId}) {
     return _events(mapId).whereType<MapLongPressEvent>();
   }
 
   /// Disposes of the current map. It can't be used afterwards!
   @override
-  void dispose({@required int mapId}) {
-    _map(mapId)?.dispose();
+  void dispose({required int mapId}) {
+    _map(mapId).dispose();
     _mapById.remove(mapId);
   }
 
@@ -278,19 +278,16 @@
   Widget buildView(
     int creationId,
     PlatformViewCreatedCallback onPlatformViewCreated, {
-    @required CameraPosition initialCameraPosition,
+    required CameraPosition initialCameraPosition,
     Set<Marker> markers = const <Marker>{},
     Set<Polygon> polygons = const <Polygon>{},
     Set<Polyline> polylines = const <Polyline>{},
     Set<Circle> circles = const <Circle>{},
     Set<TileOverlay> tileOverlays = const <TileOverlay>{},
-    Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers =
+    Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers =
         const <Factory<OneSequenceGestureRecognizer>>{},
     Map<String, dynamic> mapOptions = const <String, dynamic>{},
   }) {
-    assert(creationId != null,
-        'buildView needs a `_webOnlyMapCreationId` in its creationParams to prevent widget reloads in web.');
-
     // Bail fast if we've already rendered this map ID...
     if (_mapById[creationId]?.widget != null) {
       return _mapById[creationId].widget;
@@ -314,6 +311,9 @@
 
     onPlatformViewCreated.call(creationId);
 
-    return mapController.widget;
+    assert(mapController.widget != null,
+        'The widget of a GoogleMapController cannot be null before calling dispose on it.');
+
+    return mapController.widget!;
   }
 }
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 62238fc..5b0169b 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
@@ -6,33 +6,35 @@
 
 /// The `MarkerController` class wraps a [gmaps.Marker], how it handles events, and its associated (optional) [gmaps.InfoWindow] widget.
 class MarkerController {
-  gmaps.Marker _marker;
+  gmaps.Marker? _marker;
 
   final bool _consumeTapEvents;
 
-  final gmaps.InfoWindow _infoWindow;
+  final gmaps.InfoWindow? _infoWindow;
 
   bool _infoWindowShown = false;
 
   /// Creates a `MarkerController`, which wraps a [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its associated [gmaps.InfoWindow].
   MarkerController({
-    @required gmaps.Marker marker,
-    gmaps.InfoWindow infoWindow,
+    required gmaps.Marker marker,
+    gmaps.InfoWindow? infoWindow,
     bool consumeTapEvents = false,
-    LatLngCallback onDragEnd,
-    ui.VoidCallback onTap,
+    LatLngCallback? onDragEnd,
+    ui.VoidCallback? onTap,
   })  : _marker = marker,
         _infoWindow = infoWindow,
         _consumeTapEvents = consumeTapEvents {
     if (onTap != null) {
-      _marker.onClick.listen((event) {
+      marker.onClick.listen((event) {
         onTap.call();
       });
     }
     if (onDragEnd != null) {
-      _marker.onDragend.listen((event) {
-        _marker.position = event.latLng;
-        onDragEnd.call(event.latLng);
+      marker.onDragend.listen((event) {
+        if (marker != null) {
+          marker.position = event.latLng;
+        }
+        onDragEnd.call(event.latLng ?? _nullGmapsLatLng);
       });
     }
   }
@@ -44,42 +46,54 @@
   bool get infoWindowShown => _infoWindowShown;
 
   /// Returns the [gmaps.Marker] associated to this controller.
-  gmaps.Marker get marker => _marker;
+  gmaps.Marker? get marker => _marker;
 
   /// Returns the [gmaps.InfoWindow] associated to the marker.
   @visibleForTesting
-  gmaps.InfoWindow get infoWindow => _infoWindow;
+  gmaps.InfoWindow? get infoWindow => _infoWindow;
 
   /// Updates the options of the wrapped [gmaps.Marker] object.
+  ///
+  /// This cannot be called after [remove].
   void update(
     gmaps.MarkerOptions options, {
-    String newInfoWindowContent,
+    HtmlElement? newInfoWindowContent,
   }) {
-    _marker.options = options;
+    assert(_marker != null, 'Cannot `update` Marker after calling `remove`.');
+    _marker!.options = options;
     if (_infoWindow != null && newInfoWindowContent != null) {
-      _infoWindow.content = newInfoWindowContent;
+      _infoWindow!.content = newInfoWindowContent;
     }
   }
 
   /// Disposes of the currently wrapped [gmaps.Marker].
   void remove() {
-    _marker.visible = false;
-    _marker.map = null;
-    _marker = null;
+    if (_marker != null) {
+      _infoWindowShown = false;
+      _marker!.visible = false;
+      _marker!.map = null;
+      _marker = null;
+    }
   }
 
   /// Hide the associated [gmaps.InfoWindow].
+  ///
+  /// This cannot be called after [remove].
   void hideInfoWindow() {
+    assert(_marker != null, 'Cannot `hideInfoWindow` on a `remove`d Marker.');
     if (_infoWindow != null) {
-      _infoWindow.close();
+      _infoWindow!.close();
       _infoWindowShown = false;
     }
   }
 
   /// Show the associated [gmaps.InfoWindow].
+  ///
+  /// This cannot be called after [remove].
   void showInfoWindow() {
+    assert(_marker != null, 'Cannot `showInfoWindow` on a `remove`d Marker.');
     if (_infoWindow != null) {
-      _infoWindow.open(_marker.map, _marker);
+      _infoWindow!.open(_marker!.map, _marker);
       _infoWindowShown = true;
     }
   }
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 bc9827a..704577b 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
@@ -14,8 +14,8 @@
 
   /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers.
   MarkersController({
-    @required StreamController<MapEvent> stream,
-  })  : _streamController = stream,
+    required StreamController<MapEvent> stream,
+  })   : _streamController = stream,
         _markerIdToController = Map<MarkerId, MarkerController>();
 
   /// Returns the cache of [MarkerController]s. Test only.
@@ -26,7 +26,7 @@
   ///
   /// Wraps each [Marker] into its corresponding [MarkerController].
   void addMarkers(Set<Marker> markersToAdd) {
-    markersToAdd?.forEach(_addMarker);
+    markersToAdd.forEach(_addMarker);
   }
 
   void _addMarker(Marker marker) {
@@ -35,14 +35,15 @@
     }
 
     final infoWindowOptions = _infoWindowOptionsFromMarker(marker);
-    gmaps.InfoWindow gmInfoWindow;
+    gmaps.InfoWindow? gmInfoWindow;
 
     if (infoWindowOptions != null) {
       gmInfoWindow = gmaps.InfoWindow(infoWindowOptions);
       // Google Maps' JS SDK does not have a click event on the InfoWindow, so
       // we make one...
       if (infoWindowOptions.content is HtmlElement) {
-        infoWindowOptions.content.onClick.listen((_) {
+        final content = infoWindowOptions.content as HtmlElement;
+        content.onClick.listen((_) {
           _onInfoWindowTap(marker.markerId);
         });
       }
@@ -70,11 +71,11 @@
 
   /// Updates a set of [Marker] objects with new options.
   void changeMarkers(Set<Marker> markersToChange) {
-    markersToChange?.forEach(_changeMarker);
+    markersToChange.forEach(_changeMarker);
   }
 
   void _changeMarker(Marker marker) {
-    MarkerController markerController = _markerIdToController[marker?.markerId];
+    MarkerController? markerController = _markerIdToController[marker.markerId];
     if (markerController != null) {
       final markerOptions = _markerOptionsFromMarker(
         marker,
@@ -83,18 +84,18 @@
       final infoWindow = _infoWindowOptionsFromMarker(marker);
       markerController.update(
         markerOptions,
-        newInfoWindowContent: infoWindow?.content,
+        newInfoWindowContent: infoWindow?.content as HtmlElement?,
       );
     }
   }
 
   /// Removes a set of [MarkerId]s from the cache.
   void removeMarkers(Set<MarkerId> markerIdsToRemove) {
-    markerIdsToRemove?.forEach(_removeMarker);
+    markerIdsToRemove.forEach(_removeMarker);
   }
 
   void _removeMarker(MarkerId markerId) {
-    final MarkerController markerController = _markerIdToController[markerId];
+    final MarkerController? markerController = _markerIdToController[markerId];
     markerController?.remove();
     _markerIdToController.remove(markerId);
   }
@@ -106,7 +107,7 @@
   /// See also [hideMarkerInfoWindow] and [isInfoWindowShown].
   void showMarkerInfoWindow(MarkerId markerId) {
     _hideAllMarkerInfoWindow();
-    MarkerController markerController = _markerIdToController[markerId];
+    MarkerController? markerController = _markerIdToController[markerId];
     markerController?.showInfoWindow();
   }
 
@@ -114,7 +115,7 @@
   ///
   /// See also [showMarkerInfoWindow] and [isInfoWindowShown].
   void hideMarkerInfoWindow(MarkerId markerId) {
-    MarkerController markerController = _markerIdToController[markerId];
+    MarkerController? markerController = _markerIdToController[markerId];
     markerController?.hideInfoWindow();
   }
 
@@ -122,7 +123,7 @@
   ///
   /// See also [showMarkerInfoWindow] and [hideMarkerInfoWindow].
   bool isInfoWindowShown(MarkerId markerId) {
-    MarkerController markerController = _markerIdToController[markerId];
+    MarkerController? markerController = _markerIdToController[markerId];
     return markerController?.infoWindowShown ?? false;
   }
 
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart
index 4ce1f02..9921d2f 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart
@@ -6,15 +6,15 @@
 
 /// The `PolygonController` class wraps a [gmaps.Polygon] and its `onTap` behavior.
 class PolygonController {
-  gmaps.Polygon _polygon;
+  gmaps.Polygon? _polygon;
 
   final bool _consumeTapEvents;
 
   /// Creates a `PolygonController` that wraps a [gmaps.Polygon] object and its `onTap` behavior.
   PolygonController({
-    @required gmaps.Polygon polygon,
+    required gmaps.Polygon polygon,
     bool consumeTapEvents = false,
-    ui.VoidCallback onTap,
+    ui.VoidCallback? onTap,
   })  : _polygon = polygon,
         _consumeTapEvents = consumeTapEvents {
     if (onTap != null) {
@@ -26,20 +26,25 @@
 
   /// Returns the wrapped [gmaps.Polygon]. Only used for testing.
   @visibleForTesting
-  gmaps.Polygon get polygon => _polygon;
+  gmaps.Polygon? get polygon => _polygon;
 
   /// Returns `true` if this Controller will use its own `onTap` handler to consume events.
   bool get consumeTapEvents => _consumeTapEvents;
 
   /// Updates the options of the wrapped [gmaps.Polygon] object.
+  ///
+  /// This cannot be called after [remove].
   void update(gmaps.PolygonOptions options) {
-    _polygon.options = options;
+    assert(_polygon != null, 'Cannot `update` Polygon after calling `remove`.');
+    _polygon!.options = options;
   }
 
   /// Disposes of the currently wrapped [gmaps.Polygon].
   void remove() {
-    _polygon.visible = false;
-    _polygon.map = null;
-    _polygon = null;
+    if (_polygon != null) {
+      _polygon!.visible = false;
+      _polygon!.map = null;
+      _polygon = null;
+    }
   }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart
index 4671e6d..ef51bd6 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart
@@ -14,8 +14,8 @@
 
   /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers.
   PolygonsController({
-    @required StreamController<MapEvent> stream,
-  })  : _streamController = stream,
+    required StreamController<MapEvent> stream,
+  })   : _streamController = stream,
         _polygonIdToController = Map<PolygonId, PolygonController>();
 
   /// Returns the cache of [PolygonController]s. Test only.
@@ -60,15 +60,15 @@
   }
 
   void _changePolygon(Polygon polygon) {
-    PolygonController polygonController =
-        _polygonIdToController[polygon?.polygonId];
+    PolygonController? polygonController =
+        _polygonIdToController[polygon.polygonId];
     polygonController?.update(_polygonOptionsFromPolygon(googleMap, polygon));
   }
 
   /// Removes a set of [PolygonId]s from the cache.
   void removePolygons(Set<PolygonId> polygonIdsToRemove) {
-    polygonIdsToRemove?.forEach((polygonId) {
-      final PolygonController polygonController =
+    polygonIdsToRemove.forEach((polygonId) {
+      final PolygonController? polygonController =
           _polygonIdToController[polygonId];
       polygonController?.remove();
       _polygonIdToController.remove(polygonId);
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart
index bf1dbb2..eb4b6d8 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart
@@ -6,15 +6,15 @@
 
 /// The `PolygonController` class wraps a [gmaps.Polyline] and its `onTap` behavior.
 class PolylineController {
-  gmaps.Polyline _polyline;
+  gmaps.Polyline? _polyline;
 
   final bool _consumeTapEvents;
 
   /// Creates a `PolylineController` that wraps a [gmaps.Polyline] object and its `onTap` behavior.
   PolylineController({
-    @required gmaps.Polyline polyline,
+    required gmaps.Polyline polyline,
     bool consumeTapEvents = false,
-    ui.VoidCallback onTap,
+    ui.VoidCallback? onTap,
   })  : _polyline = polyline,
         _consumeTapEvents = consumeTapEvents {
     if (onTap != null) {
@@ -26,20 +26,26 @@
 
   /// Returns the wrapped [gmaps.Polyline]. Only used for testing.
   @visibleForTesting
-  gmaps.Polyline get line => _polyline;
+  gmaps.Polyline? get line => _polyline;
 
   /// Returns `true` if this Controller will use its own `onTap` handler to consume events.
   bool get consumeTapEvents => _consumeTapEvents;
 
   /// Updates the options of the wrapped [gmaps.Polyline] object.
+  ///
+  /// This cannot be called after [remove].
   void update(gmaps.PolylineOptions options) {
-    _polyline.options = options;
+    assert(
+        _polyline != null, 'Cannot `update` Polyline after calling `remove`.');
+    _polyline!.options = options;
   }
 
   /// Disposes of the currently wrapped [gmaps.Polyline].
   void remove() {
-    _polyline.visible = false;
-    _polyline.map = null;
-    _polyline = null;
+    if (_polyline != null) {
+      _polyline!.visible = false;
+      _polyline!.map = null;
+      _polyline = null;
+    }
   }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart
index e91b82f..184c0d9 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart
@@ -14,8 +14,8 @@
 
   /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers.
   PolylinesController({
-    @required StreamController<MapEvent> stream,
-  })  : _streamController = stream,
+    required StreamController<MapEvent> stream,
+  })   : _streamController = stream,
         _polylineIdToController = Map<PolylineId, PolylineController>();
 
   /// Returns the cache of [PolylineContrller]s. Test only.
@@ -26,7 +26,7 @@
   ///
   /// Wraps each line into its corresponding [PolylineController].
   void addPolylines(Set<Polyline> polylinesToAdd) {
-    polylinesToAdd?.forEach((polyline) {
+    polylinesToAdd.forEach((polyline) {
       _addPolyline(polyline);
     });
   }
@@ -50,22 +50,22 @@
 
   /// Updates a set of [Polyline] objects with new options.
   void changePolylines(Set<Polyline> polylinesToChange) {
-    polylinesToChange?.forEach((polylineToChange) {
+    polylinesToChange.forEach((polylineToChange) {
       _changePolyline(polylineToChange);
     });
   }
 
   void _changePolyline(Polyline polyline) {
-    PolylineController polylineController =
-        _polylineIdToController[polyline?.polylineId];
+    PolylineController? polylineController =
+        _polylineIdToController[polyline.polylineId];
     polylineController
         ?.update(_polylineOptionsFromPolyline(googleMap, polyline));
   }
 
   /// Removes a set of [PolylineId]s from the cache.
   void removePolylines(Set<PolylineId> polylineIdsToRemove) {
-    polylineIdsToRemove?.forEach((polylineId) {
-      final PolylineController polylineController =
+    polylineIdsToRemove.forEach((polylineId) {
+      final PolylineController? polylineController =
           _polylineIdToController[polylineId];
       polylineController?.remove();
       _polylineIdToController.remove(polylineId);
diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart
index 10b5199..ff980eb 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart
@@ -17,10 +17,10 @@
 /// instance and our internal `mapId` value.
 abstract class GeometryController {
   /// The GMap instance that this controller operates on.
-  gmaps.GMap googleMap;
+  late gmaps.GMap googleMap;
 
   /// The map ID for events.
-  int mapId;
+  late int mapId;
 
   /// Binds a `mapId` and the [gmaps.GMap] instance to this controller.
   void bindToMap(int mapId, gmaps.GMap googleMap) {
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 22df1b2..5312a56 100644
--- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml
@@ -1,7 +1,7 @@
 name: google_maps_flutter_web
 description: Web platform implementation of google_maps_flutter
 homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter
-version: 0.2.1
+version: 0.3.0
 
 flutter:
   plugin:
@@ -17,7 +17,7 @@
     sdk: flutter
   meta: ^1.3.0
   google_maps_flutter_platform_interface: ^2.0.1
-  google_maps: ^3.4.5
+  google_maps: ^5.1.0
   stream_transform: ^2.0.0
   sanitize_html: ^2.0.0
 
@@ -27,5 +27,5 @@
   pedantic: ^1.10.0
 
 environment:
-  sdk: ">=2.3.0 <3.0.0"
+  sdk: ">=2.12.0 <3.0.0"
   flutter: ">=1.20.0"
diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh
index 56b0585..06566f0 100755
--- a/script/build_all_plugins_app.sh
+++ b/script/build_all_plugins_app.sh
@@ -59,7 +59,7 @@
 
 for version in "${BUILD_MODES[@]}"; do
   echo "Building $version..."
-  (cd $REPO_DIR/all_plugins && flutter build $@ --$version --no-sound-null-safety)
+  (cd $REPO_DIR/all_plugins && flutter build $@ --$version)
 
   if [ $? -eq 0 ]; then
     echo "Successfully built $version all_plugins app."