[google_maps_flutter] Migrate platform interface to null safety (#3539)

Part of flutter/flutter#75236

Includes a significant refactor of the various overlay objects to remove a lot of identical code across the objects.
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
index 4273f59..c530c31 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 2.0.0-nullsafety
+
+* Migrated to null-safety.
+* BREAKING CHANGE: Removed deprecated APIs.
+* BREAKING CHANGE: Many sets in APIs that used to treat null and empty set as
+  equivalent now require passing an empty set.
+* BREAKING CHANGE: toJson now always returns an `Object`; the details of the
+  object type and structure should be treated as an implementation detail.
+
 ## 1.2.0
 
 * Add TileOverlay support.
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart
index 8b7af2c..3d16127 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart
@@ -15,6 +15,26 @@
 import '../types/tile_overlay_updates.dart';
 import '../types/utils/tile_overlay.dart';
 
+/// Error thrown when an unknown map ID is provided to a method channel API.
+class UnknownMapIDError extends Error {
+  /// Creates an assertion error with the provided [mapId] and optional
+  /// [message].
+  UnknownMapIDError(this.mapId, [this.message]);
+
+  /// The unknown ID.
+  final int mapId;
+
+  /// Message describing the assertion error.
+  final Object? message;
+
+  String toString() {
+    if (message != null) {
+      return "Unknown map ID $mapId: ${Error.safeToString(message)}";
+    }
+    return "Unknown map ID $mapId";
+  }
+}
+
 /// An implementation of [GoogleMapsFlutterPlatform] that uses [MethodChannel] to communicate with the native code.
 ///
 /// The `google_maps_flutter` plugin code itself never talks to the native code directly. It delegates
@@ -32,19 +52,20 @@
 
   /// Accesses the MethodChannel associated to the passed mapId.
   MethodChannel channel(int mapId) {
-    return _channels[mapId];
+    MethodChannel? channel = _channels[mapId];
+    if (channel == null) {
+      throw UnknownMapIDError(mapId);
+    }
+    return channel;
   }
 
   // Keep a collection of mapId to a map of TileOverlays.
   final Map<int, Map<TileOverlayId, TileOverlay>> _tileOverlays = {};
 
-  /// Initializes the platform interface with [id].
-  ///
-  /// This method is called when the plugin is first initialized.
   @override
   Future<void> init(int mapId) {
-    MethodChannel channel;
-    if (!_channels.containsKey(mapId)) {
+    MethodChannel? channel = _channels[mapId];
+    if (channel == null) {
       channel = MethodChannel('plugins.flutter.io/google_maps_$mapId');
       channel.setMethodCallHandler(
           (MethodCall call) => _handleMethodCall(call, mapId));
@@ -53,9 +74,8 @@
     return channel.invokeMethod<void>('map#waitForMap');
   }
 
-  /// Dispose of the native resources.
   @override
-  void dispose({int mapId}) {
+  void dispose({required int mapId}) {
     // Noop!
   }
 
@@ -72,57 +92,57 @@
       _mapEventStreamController.stream.where((event) => event.mapId == mapId);
 
   @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>();
   }
 
@@ -134,7 +154,7 @@
       case 'camera#onMove':
         _mapEventStreamController.add(CameraMoveEvent(
           mapId,
-          CameraPosition.fromMap(call.arguments['position']),
+          CameraPosition.fromMap(call.arguments['position'])!,
         ));
         break;
       case 'camera#onIdle':
@@ -149,7 +169,7 @@
       case 'marker#onDragEnd':
         _mapEventStreamController.add(MarkerDragEndEvent(
           mapId,
-          LatLng.fromJson(call.arguments['position']),
+          LatLng.fromJson(call.arguments['position'])!,
           MarkerId(call.arguments['markerId']),
         ));
         break;
@@ -180,26 +200,26 @@
       case 'map#onTap':
         _mapEventStreamController.add(MapTapEvent(
           mapId,
-          LatLng.fromJson(call.arguments['position']),
+          LatLng.fromJson(call.arguments['position'])!,
         ));
         break;
       case 'map#onLongPress':
         _mapEventStreamController.add(MapLongPressEvent(
           mapId,
-          LatLng.fromJson(call.arguments['position']),
+          LatLng.fromJson(call.arguments['position'])!,
         ));
         break;
       case 'tileOverlay#getTile':
-        final Map<TileOverlayId, TileOverlay> tileOverlaysForThisMap =
+        final Map<TileOverlayId, TileOverlay>? tileOverlaysForThisMap =
             _tileOverlays[mapId];
         final String tileOverlayId = call.arguments['tileOverlayId'];
-        final TileOverlay tileOverlay =
-            tileOverlaysForThisMap[TileOverlayId(tileOverlayId)];
-        Tile tile;
-        if (tileOverlay == null || tileOverlay.tileProvider == null) {
+        final TileOverlay? tileOverlay =
+            tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)];
+        TileProvider? tileProvider = tileOverlay?.tileProvider;
+        if (tileProvider == null) {
           return TileProvider.noTile.toJson();
         }
-        tile = await tileOverlay.tileProvider.getTile(
+        final Tile tile = await tileProvider.getTile(
           call.arguments['x'],
           call.arguments['y'],
           call.arguments['zoom'],
@@ -210,16 +230,10 @@
     }
   }
 
-  /// Updates configuration options of the map user interface.
-  ///
-  /// Change listeners are notified once the update has been made on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes after listeners have been notified.
   @override
   Future<void> updateMapOptions(
     Map<String, dynamic> optionsUpdate, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(optionsUpdate != null);
     return channel(mapId).invokeMethod<void>(
@@ -230,16 +244,10 @@
     );
   }
 
-  /// Updates marker configuration.
-  ///
-  /// Change listeners are notified once the update has been made on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes after listeners have been notified.
   @override
   Future<void> updateMarkers(
     MarkerUpdates markerUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(markerUpdates != null);
     return channel(mapId).invokeMethod<void>(
@@ -248,16 +256,10 @@
     );
   }
 
-  /// Updates polygon configuration.
-  ///
-  /// Change listeners are notified once the update has been made on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes after listeners have been notified.
   @override
   Future<void> updatePolygons(
     PolygonUpdates polygonUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(polygonUpdates != null);
     return channel(mapId).invokeMethod<void>(
@@ -266,16 +268,10 @@
     );
   }
 
-  /// Updates polyline configuration.
-  ///
-  /// Change listeners are notified once the update has been made on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes after listeners have been notified.
   @override
   Future<void> updatePolylines(
     PolylineUpdates polylineUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(polylineUpdates != null);
     return channel(mapId).invokeMethod<void>(
@@ -284,16 +280,10 @@
     );
   }
 
-  /// Updates circle configuration.
-  ///
-  /// Change listeners are notified once the update has been made on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes after listeners have been notified.
   @override
   Future<void> updateCircles(
     CircleUpdates circleUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(circleUpdates != null);
     return channel(mapId).invokeMethod<void>(
@@ -302,23 +292,16 @@
     );
   }
 
-  /// Updates tile overlay configuration.
-  ///
-  /// Change listeners are notified once the update has been made on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes after listeners have been notified.
-  ///
-  /// If `newTileOverlays` is null, all the [TileOverlays] are removed for the Map with `mapId`.
   @override
   Future<void> updateTileOverlays({
-    Set<TileOverlay> newTileOverlays,
-    @required int mapId,
+    required Set<TileOverlay> newTileOverlays,
+    required int mapId,
   }) {
-    final Map<TileOverlayId, TileOverlay> currentTileOverlays =
+    final Map<TileOverlayId, TileOverlay>? currentTileOverlays =
         _tileOverlays[mapId];
-    Set<TileOverlay> previousSet =
-        currentTileOverlays != null ? currentTileOverlays.values.toSet() : null;
+    Set<TileOverlay> previousSet = currentTileOverlays != null
+        ? currentTileOverlays.values.toSet()
+        : <TileOverlay>{};
     final TileOverlayUpdates updates =
         TileOverlayUpdates.from(previousSet, newTileOverlays);
     _tileOverlays[mapId] = keyTileOverlayId(newTileOverlays);
@@ -328,203 +311,152 @@
     );
   }
 
-  /// Clears the tile cache so that all tiles will be requested again from the
-  /// [TileProvider].
-  ///
-  /// The current tiles from this tile overlay will also be
-  /// cleared from the map after calling this method. The Google Map SDK maintains a small
-  /// in-memory cache of tiles. If you want to cache tiles for longer, you
-  /// should implement an on-disk cache.
   @override
   Future<void> clearTileCache(
     TileOverlayId tileOverlayId, {
-    @required int mapId,
+    required int mapId,
   }) {
     return channel(mapId)
-        .invokeMethod<void>('tileOverlays#clearTileCache', <String, dynamic>{
+        .invokeMethod<void>('tileOverlays#clearTileCache', <String, Object>{
       'tileOverlayId': tileOverlayId.value,
     });
   }
 
-  /// Starts an animated change of the map camera position.
-  ///
-  /// The returned [Future] completes after the change has been started on the
-  /// platform side.
   @override
   Future<void> animateCamera(
     CameraUpdate cameraUpdate, {
-    @required int mapId,
+    required int mapId,
   }) {
-    return channel(mapId)
-        .invokeMethod<void>('camera#animate', <String, dynamic>{
+    return channel(mapId).invokeMethod<void>('camera#animate', <String, Object>{
       'cameraUpdate': cameraUpdate.toJson(),
     });
   }
 
-  /// Changes the map camera position.
-  ///
-  /// The returned [Future] completes after the change has been made on the
-  /// platform side.
   @override
   Future<void> moveCamera(
     CameraUpdate cameraUpdate, {
-    @required int mapId,
+    required int mapId,
   }) {
     return channel(mapId).invokeMethod<void>('camera#move', <String, dynamic>{
       'cameraUpdate': cameraUpdate.toJson(),
     });
   }
 
-  /// Sets the styling of the base map.
-  ///
-  /// Set to `null` to clear any previous custom styling.
-  ///
-  /// If problems were detected with the [mapStyle], including un-parsable
-  /// styling JSON, unrecognized feature type, unrecognized element type, or
-  /// invalid styler keys: [MapStyleException] is thrown and the current
-  /// style is left unchanged.
-  ///
-  /// The style string can be generated using [map style tool](https://mapstyle.withgoogle.com/).
-  /// Also, refer [iOS](https://developers.google.com/maps/documentation/ios-sdk/style-reference)
-  /// and [Android](https://developers.google.com/maps/documentation/android-sdk/style-reference)
-  /// style reference for more information regarding the supported styles.
   @override
   Future<void> setMapStyle(
-    String mapStyle, {
-    @required int mapId,
+    String? mapStyle, {
+    required int mapId,
   }) async {
-    final List<dynamic> successAndError = await channel(mapId)
-        .invokeMethod<List<dynamic>>('map#setStyle', mapStyle);
+    final List<dynamic> successAndError = (await channel(mapId)
+        .invokeMethod<List<dynamic>>('map#setStyle', mapStyle))!;
     final bool success = successAndError[0];
     if (!success) {
       throw MapStyleException(successAndError[1]);
     }
   }
 
-  /// Return the region that is visible in a map.
   @override
   Future<LatLngBounds> getVisibleRegion({
-    @required int mapId,
+    required int mapId,
   }) async {
-    final Map<String, dynamic> latLngBounds = await channel(mapId)
-        .invokeMapMethod<String, dynamic>('map#getVisibleRegion');
-    final LatLng southwest = LatLng.fromJson(latLngBounds['southwest']);
-    final LatLng northeast = LatLng.fromJson(latLngBounds['northeast']);
+    final Map<String, dynamic> latLngBounds = (await channel(mapId)
+        .invokeMapMethod<String, dynamic>('map#getVisibleRegion'))!;
+    final LatLng southwest = LatLng.fromJson(latLngBounds['southwest'])!;
+    final LatLng northeast = LatLng.fromJson(latLngBounds['northeast'])!;
 
     return LatLngBounds(northeast: northeast, southwest: southwest);
   }
 
-  /// Return point [Map<String, int>] of the [screenCoordinateInJson] in the current map view.
-  ///
-  /// A projection is used to translate between on screen location and geographic coordinates.
-  /// Screen location is in screen pixels (not display pixels) with respect to the top left corner
-  /// of the map, not necessarily of the whole screen.
   @override
   Future<ScreenCoordinate> getScreenCoordinate(
     LatLng latLng, {
-    @required int mapId,
+    required int mapId,
   }) async {
-    final Map<String, int> point = await channel(mapId)
+    final Map<String, int> point = (await channel(mapId)
         .invokeMapMethod<String, int>(
-            'map#getScreenCoordinate', latLng.toJson());
+            'map#getScreenCoordinate', latLng.toJson()))!;
 
-    return ScreenCoordinate(x: point['x'], y: point['y']);
+    return ScreenCoordinate(x: point['x']!, y: point['y']!);
   }
 
-  /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view.
-  ///
-  /// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen
-  /// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen.
   @override
   Future<LatLng> getLatLng(
     ScreenCoordinate screenCoordinate, {
-    @required int mapId,
+    required int mapId,
   }) async {
-    final List<dynamic> latLng = await channel(mapId)
+    final List<dynamic> latLng = (await channel(mapId)
         .invokeMethod<List<dynamic>>(
-            'map#getLatLng', screenCoordinate.toJson());
+            'map#getLatLng', screenCoordinate.toJson()))!;
     return LatLng(latLng[0], latLng[1]);
   }
 
-  /// Programmatically show the Info Window for a [Marker].
-  ///
-  /// The `markerId` must match one of the markers on the map.
-  /// An invalid `markerId` triggers an "Invalid markerId" error.
-  ///
-  /// * See also:
-  ///   * [hideMarkerInfoWindow] to hide the Info Window.
-  ///   * [isMarkerInfoWindowShown] to check if the Info Window is showing.
   @override
   Future<void> showMarkerInfoWindow(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(markerId != null);
     return channel(mapId).invokeMethod<void>(
         'markers#showInfoWindow', <String, String>{'markerId': markerId.value});
   }
 
-  /// Programmatically hide the Info Window for a [Marker].
-  ///
-  /// The `markerId` must match one of the markers on the map.
-  /// An invalid `markerId` triggers an "Invalid markerId" error.
-  ///
-  /// * See also:
-  ///   * [showMarkerInfoWindow] to show the Info Window.
-  ///   * [isMarkerInfoWindowShown] to check if the Info Window is showing.
   @override
   Future<void> hideMarkerInfoWindow(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(markerId != null);
     return channel(mapId).invokeMethod<void>(
         'markers#hideInfoWindow', <String, String>{'markerId': markerId.value});
   }
 
-  /// Returns `true` when the [InfoWindow] is showing, `false` otherwise.
-  ///
-  /// The `markerId` must match one of the markers on the map.
-  /// An invalid `markerId` triggers an "Invalid markerId" error.
-  ///
-  /// * See also:
-  ///   * [showMarkerInfoWindow] to show the Info Window.
-  ///   * [hideMarkerInfoWindow] to hide the Info Window.
   @override
   Future<bool> isMarkerInfoWindowShown(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) {
     assert(markerId != null);
     return channel(mapId).invokeMethod<bool>('markers#isInfoWindowShown',
-        <String, String>{'markerId': markerId.value});
+        <String, String>{'markerId': markerId.value}) as Future<bool>;
   }
 
-  /// Returns the current zoom level of the map
   @override
   Future<double> getZoomLevel({
-    @required int mapId,
+    required int mapId,
   }) {
-    return channel(mapId).invokeMethod<double>('map#getZoomLevel');
+    return channel(mapId).invokeMethod<double>('map#getZoomLevel')
+        as Future<double>;
   }
 
-  /// Returns the image bytes of the map
   @override
-  Future<Uint8List> takeSnapshot({
-    @required int mapId,
+  Future<Uint8List?> takeSnapshot({
+    required int mapId,
   }) {
     return channel(mapId).invokeMethod<Uint8List>('map#takeSnapshot');
   }
 
-  /// This method builds the appropriate platform view where the map
-  /// can be rendered.
-  /// The `mapId` is passed as a parameter from the framework on the
-  /// `onPlatformViewCreated` callback.
   @override
   Widget buildView(
-      Map<String, dynamic> creationParams,
-      Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
-      PlatformViewCreatedCallback onPlatformViewCreated) {
+    int creationId,
+    PlatformViewCreatedCallback onPlatformViewCreated, {
+    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,
+    Map<String, dynamic> mapOptions = const <String, dynamic>{},
+  }) {
+    final Map<String, dynamic> creationParams = <String, dynamic>{
+      'initialCameraPosition': initialCameraPosition.toMap(),
+      'options': mapOptions,
+      'markersToAdd': serializeMarkerSet(markers),
+      'polygonsToAdd': serializePolygonSet(polygons),
+      'polylinesToAdd': serializePolylineSet(polylines),
+      'circlesToAdd': serializeCircleSet(circles),
+      'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays),
+    };
     if (defaultTargetPlatform == TargetPlatform.android) {
       return AndroidView(
         viewType: 'plugins.flutter.io/google_maps',
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart
index b9d3fec..a363fc3 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart
@@ -58,7 +58,7 @@
   /// The returned [Future] completes after listeners have been notified.
   Future<void> updateMapOptions(
     Map<String, dynamic> optionsUpdate, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('updateMapOptions() has not been implemented.');
   }
@@ -71,7 +71,7 @@
   /// The returned [Future] completes after listeners have been notified.
   Future<void> updateMarkers(
     MarkerUpdates markerUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('updateMarkers() has not been implemented.');
   }
@@ -84,7 +84,7 @@
   /// The returned [Future] completes after listeners have been notified.
   Future<void> updatePolygons(
     PolygonUpdates polygonUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('updatePolygons() has not been implemented.');
   }
@@ -97,7 +97,7 @@
   /// The returned [Future] completes after listeners have been notified.
   Future<void> updatePolylines(
     PolylineUpdates polylineUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('updatePolylines() has not been implemented.');
   }
@@ -110,7 +110,7 @@
   /// The returned [Future] completes after listeners have been notified.
   Future<void> updateCircles(
     CircleUpdates circleUpdates, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('updateCircles() has not been implemented.');
   }
@@ -122,8 +122,8 @@
   ///
   /// The returned [Future] completes after listeners have been notified.
   Future<void> updateTileOverlays({
-    Set<TileOverlay> newTileOverlays,
-    @required int mapId,
+    required Set<TileOverlay> newTileOverlays,
+    required int mapId,
   }) {
     throw UnimplementedError('updateTileOverlays() has not been implemented.');
   }
@@ -137,7 +137,7 @@
   /// should implement an on-disk cache.
   Future<void> clearTileCache(
     TileOverlayId tileOverlayId, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('clearTileCache() has not been implemented.');
   }
@@ -148,7 +148,7 @@
   /// platform side.
   Future<void> animateCamera(
     CameraUpdate cameraUpdate, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('animateCamera() has not been implemented.');
   }
@@ -159,7 +159,7 @@
   /// platform side.
   Future<void> moveCamera(
     CameraUpdate cameraUpdate, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('moveCamera() has not been implemented.');
   }
@@ -175,15 +175,15 @@
   ///
   /// The style string can be generated using [map style tool](https://mapstyle.withgoogle.com/).
   Future<void> setMapStyle(
-    String mapStyle, {
-    @required int mapId,
+    String? mapStyle, {
+    required int mapId,
   }) {
     throw UnimplementedError('setMapStyle() has not been implemented.');
   }
 
   /// Return the region that is visible in a map.
   Future<LatLngBounds> getVisibleRegion({
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('getVisibleRegion() has not been implemented.');
   }
@@ -195,7 +195,7 @@
   /// of the map, not necessarily of the whole screen.
   Future<ScreenCoordinate> getScreenCoordinate(
     LatLng latLng, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('getScreenCoordinate() has not been implemented.');
   }
@@ -207,7 +207,7 @@
   /// of the map, not necessarily of the whole screen.
   Future<LatLng> getLatLng(
     ScreenCoordinate screenCoordinate, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('getLatLng() has not been implemented.');
   }
@@ -222,7 +222,7 @@
   ///   * [isMarkerInfoWindowShown] to check if the Info Window is showing.
   Future<void> showMarkerInfoWindow(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError(
         'showMarkerInfoWindow() has not been implemented.');
@@ -238,7 +238,7 @@
   ///   * [isMarkerInfoWindowShown] to check if the Info Window is showing.
   Future<void> hideMarkerInfoWindow(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError(
         'hideMarkerInfoWindow() has not been implemented.');
@@ -254,21 +254,23 @@
   ///   * [hideMarkerInfoWindow] to hide the Info Window.
   Future<bool> isMarkerInfoWindowShown(
     MarkerId markerId, {
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('updateMapOptions() has not been implemented.');
   }
 
-  /// Returns the current zoom level of the map
+  /// Returns the current zoom level of the map.
   Future<double> getZoomLevel({
-    @required int mapId,
+    required int mapId,
   }) {
     throw UnimplementedError('getZoomLevel() has not been implemented.');
   }
 
-  /// Returns the image bytes of the map
-  Future<Uint8List> takeSnapshot({
-    @required int mapId,
+  /// Returns the image bytes of the map.
+  ///
+  /// Returns null if a snapshot cannot be created.
+  Future<Uint8List?> takeSnapshot({
+    required int mapId,
   }) {
     throw UnimplementedError('takeSnapshot() has not been implemented.');
   }
@@ -277,70 +279,81 @@
   // into the plugin
 
   /// The Camera started moving.
-  Stream<CameraMoveStartedEvent> onCameraMoveStarted({@required int mapId}) {
+  Stream<CameraMoveStartedEvent> onCameraMoveStarted({required int mapId}) {
     throw UnimplementedError('onCameraMoveStarted() has not been implemented.');
   }
 
   /// The Camera finished moving to a new [CameraPosition].
-  Stream<CameraMoveEvent> onCameraMove({@required int mapId}) {
+  Stream<CameraMoveEvent> onCameraMove({required int mapId}) {
     throw UnimplementedError('onCameraMove() has not been implemented.');
   }
 
   /// The Camera is now idle.
-  Stream<CameraIdleEvent> onCameraIdle({@required int mapId}) {
+  Stream<CameraIdleEvent> onCameraIdle({required int mapId}) {
     throw UnimplementedError('onCameraMove() has not been implemented.');
   }
 
   /// A [Marker] has been tapped.
-  Stream<MarkerTapEvent> onMarkerTap({@required int mapId}) {
+  Stream<MarkerTapEvent> onMarkerTap({required int mapId}) {
     throw UnimplementedError('onMarkerTap() has not been implemented.');
   }
 
   /// An [InfoWindow] has been tapped.
-  Stream<InfoWindowTapEvent> onInfoWindowTap({@required int mapId}) {
+  Stream<InfoWindowTapEvent> onInfoWindowTap({required int mapId}) {
     throw UnimplementedError('onInfoWindowTap() has not been implemented.');
   }
 
   /// A [Marker] has been dragged to a different [LatLng] position.
-  Stream<MarkerDragEndEvent> onMarkerDragEnd({@required int mapId}) {
+  Stream<MarkerDragEndEvent> onMarkerDragEnd({required int mapId}) {
     throw UnimplementedError('onMarkerDragEnd() has not been implemented.');
   }
 
   /// A [Polyline] has been tapped.
-  Stream<PolylineTapEvent> onPolylineTap({@required int mapId}) {
+  Stream<PolylineTapEvent> onPolylineTap({required int mapId}) {
     throw UnimplementedError('onPolylineTap() has not been implemented.');
   }
 
   /// A [Polygon] has been tapped.
-  Stream<PolygonTapEvent> onPolygonTap({@required int mapId}) {
+  Stream<PolygonTapEvent> onPolygonTap({required int mapId}) {
     throw UnimplementedError('onPolygonTap() has not been implemented.');
   }
 
   /// A [Circle] has been tapped.
-  Stream<CircleTapEvent> onCircleTap({@required int mapId}) {
+  Stream<CircleTapEvent> onCircleTap({required int mapId}) {
     throw UnimplementedError('onCircleTap() has not been implemented.');
   }
 
   /// A Map has been tapped at a certain [LatLng].
-  Stream<MapTapEvent> onTap({@required int mapId}) {
+  Stream<MapTapEvent> onTap({required int mapId}) {
     throw UnimplementedError('onTap() has not been implemented.');
   }
 
   /// A Map has been long-pressed at a certain [LatLng].
-  Stream<MapLongPressEvent> onLongPress({@required int mapId}) {
+  Stream<MapLongPressEvent> onLongPress({required int mapId}) {
     throw UnimplementedError('onLongPress() has not been implemented.');
   }
 
   /// Dispose of whatever resources the `mapId` is holding on to.
-  void dispose({@required int mapId}) {
+  void dispose({required int mapId}) {
     throw UnimplementedError('dispose() has not been implemented.');
   }
 
   /// Returns a widget displaying the map view
   Widget buildView(
-      Map<String, dynamic> creationParams,
-      Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
-      PlatformViewCreatedCallback onPlatformViewCreated) {
+    int creationId,
+    PlatformViewCreatedCallback onPlatformViewCreated, {
+    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 =
+        const <Factory<OneSequenceGestureRecognizer>>{},
+    // TODO: Replace with a structured type that's part of the interface.
+    // See https://github.com/flutter/flutter/issues/70330.
+    Map<String, dynamic> mapOptions = const <String, dynamic>{},
+  }) {
     throw UnimplementedError('buildView() has not been implemented.');
   }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart
index e10481e..cc98875 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async' show Future;
 import 'dart:typed_data' show Uint8List;
+import 'dart:ui' show Size;
 
 import 'package:flutter/material.dart'
     show ImageConfiguration, AssetImage, AssetBundleImageKey;
@@ -61,28 +62,14 @@
 
   /// Creates a BitmapDescriptor that refers to the default marker image.
   static const BitmapDescriptor defaultMarker =
-      BitmapDescriptor._(<dynamic>[_defaultMarker]);
+      BitmapDescriptor._(<Object>[_defaultMarker]);
 
   /// Creates a BitmapDescriptor that refers to a colorization of the default
   /// marker image. For convenience, there is a predefined set of hue values.
   /// See e.g. [hueYellow].
   static BitmapDescriptor defaultMarkerWithHue(double hue) {
     assert(0.0 <= hue && hue < 360.0);
-    return BitmapDescriptor._(<dynamic>[_defaultMarker, hue]);
-  }
-
-  /// Creates a BitmapDescriptor using the name of a bitmap image in the assets
-  /// directory.
-  ///
-  /// Use [fromAssetImage]. This method does not respect the screen dpi when
-  /// picking an asset image.
-  @Deprecated("Use fromAssetImage instead")
-  static BitmapDescriptor fromAsset(String assetName, {String package}) {
-    if (package == null) {
-      return BitmapDescriptor._(<dynamic>[_fromAsset, assetName]);
-    } else {
-      return BitmapDescriptor._(<dynamic>[_fromAsset, assetName, package]);
-    }
+    return BitmapDescriptor._(<Object>[_defaultMarker, hue]);
   }
 
   /// Creates a [BitmapDescriptor] from an asset image.
@@ -95,29 +82,31 @@
   static Future<BitmapDescriptor> fromAssetImage(
     ImageConfiguration configuration,
     String assetName, {
-    AssetBundle bundle,
-    String package,
+    AssetBundle? bundle,
+    String? package,
     bool mipmaps = true,
   }) async {
-    if (!mipmaps && configuration.devicePixelRatio != null) {
-      return BitmapDescriptor._(<dynamic>[
+    double? devicePixelRatio = configuration.devicePixelRatio;
+    if (!mipmaps && devicePixelRatio != null) {
+      return BitmapDescriptor._(<Object>[
         _fromAssetImage,
         assetName,
-        configuration.devicePixelRatio,
+        devicePixelRatio,
       ]);
     }
     final AssetImage assetImage =
         AssetImage(assetName, package: package, bundle: bundle);
     final AssetBundleImageKey assetBundleImageKey =
         await assetImage.obtainKey(configuration);
-    return BitmapDescriptor._(<dynamic>[
+    final Size? size = configuration.size;
+    return BitmapDescriptor._(<Object>[
       _fromAssetImage,
       assetBundleImageKey.name,
       assetBundleImageKey.scale,
-      if (kIsWeb && configuration?.size != null)
+      if (kIsWeb && size != null)
         [
-          configuration.size.width,
-          configuration.size.height,
+          size.width,
+          size.height,
         ],
     ]);
   }
@@ -125,45 +114,47 @@
   /// Creates a BitmapDescriptor using an array of bytes that must be encoded
   /// as PNG.
   static BitmapDescriptor fromBytes(Uint8List byteData) {
-    return BitmapDescriptor._(<dynamic>[_fromBytes, byteData]);
+    return BitmapDescriptor._(<Object>[_fromBytes, byteData]);
   }
 
   /// The inverse of .toJson.
   // This is needed in Web to re-hydrate BitmapDescriptors that have been
   // transformed to JSON for transport.
   // TODO(https://github.com/flutter/flutter/issues/70330): Clean this up.
-  BitmapDescriptor.fromJson(dynamic json) : _json = json {
-    assert(_validTypes.contains(_json[0]));
-    switch (_json[0]) {
+  BitmapDescriptor.fromJson(Object json) : _json = json {
+    assert(_json is List<dynamic>);
+    final jsonList = json as List<dynamic>;
+    assert(_validTypes.contains(jsonList[0]));
+    switch (jsonList[0]) {
       case _defaultMarker:
-        assert(_json.length <= 2);
-        if (_json.length == 2) {
-          assert(_json[1] is num);
-          assert(0 <= _json[1] && _json[1] < 360);
+        assert(jsonList.length <= 2);
+        if (jsonList.length == 2) {
+          assert(jsonList[1] is num);
+          assert(0 <= jsonList[1] && jsonList[1] < 360);
         }
         break;
       case _fromBytes:
-        assert(_json.length == 2);
-        assert(_json[1] != null && _json[1] is List<int>);
-        assert((_json[1] as List).isNotEmpty);
+        assert(jsonList.length == 2);
+        assert(jsonList[1] != null && jsonList[1] is List<int>);
+        assert((jsonList[1] as List).isNotEmpty);
         break;
       case _fromAsset:
-        assert(_json.length <= 3);
-        assert(_json[1] != null && _json[1] is String);
-        assert((_json[1] as String).isNotEmpty);
-        if (_json.length == 3) {
-          assert(_json[2] != null && _json[2] is String);
-          assert((_json[2] as String).isNotEmpty);
+        assert(jsonList.length <= 3);
+        assert(jsonList[1] != null && jsonList[1] is String);
+        assert((jsonList[1] as String).isNotEmpty);
+        if (jsonList.length == 3) {
+          assert(jsonList[2] != null && jsonList[2] is String);
+          assert((jsonList[2] as String).isNotEmpty);
         }
         break;
       case _fromAssetImage:
-        assert(_json.length <= 4);
-        assert(_json[1] != null && _json[1] is String);
-        assert((_json[1] as String).isNotEmpty);
-        assert(_json[2] != null && _json[2] is double);
-        if (_json.length == 4) {
-          assert(_json[3] != null && _json[3] is List);
-          assert((_json[3] as List).length == 2);
+        assert(jsonList.length <= 4);
+        assert(jsonList[1] != null && jsonList[1] is String);
+        assert((jsonList[1] as String).isNotEmpty);
+        assert(jsonList[2] != null && jsonList[2] is double);
+        if (jsonList.length == 4) {
+          assert(jsonList[3] != null && jsonList[3] is List);
+          assert((jsonList[3] as List).length == 2);
         }
         break;
       default:
@@ -171,8 +162,8 @@
     }
   }
 
-  final dynamic _json;
+  final Object _json;
 
   /// Convert the object to a Json format.
-  dynamic toJson() => _json;
+  Object toJson() => _json;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart
index 10ea1e9..bdb0395 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart
@@ -4,8 +4,6 @@
 
 import 'dart:ui' show hashValues, Offset;
 
-import 'package:meta/meta.dart' show required;
-
 import 'types.dart';
 
 /// The position of the map "camera", the view point from which the world is shown in the map view.
@@ -19,7 +17,7 @@
   /// null.
   const CameraPosition({
     this.bearing = 0.0,
-    @required this.target,
+    required this.target,
     this.tilt = 0.0,
     this.zoom = 0.0,
   })  : assert(bearing != null),
@@ -63,7 +61,7 @@
   /// Serializes [CameraPosition].
   ///
   /// Mainly for internal use when calling [CameraUpdate.newCameraPosition].
-  dynamic toMap() => <String, dynamic>{
+  Object toMap() => <String, Object>{
         'bearing': bearing,
         'target': target.toJson(),
         'tilt': tilt,
@@ -73,23 +71,27 @@
   /// Deserializes [CameraPosition] from a map.
   ///
   /// Mainly for internal use.
-  static CameraPosition fromMap(dynamic json) {
-    if (json == null) {
+  static CameraPosition? fromMap(Object? json) {
+    if (json == null || !(json is Map<String, dynamic>)) {
+      return null;
+    }
+    final LatLng? target = LatLng.fromJson(json['target']);
+    if (target == null) {
       return null;
     }
     return CameraPosition(
       bearing: json['bearing'],
-      target: LatLng.fromJson(json['target']),
+      target: target,
       tilt: json['tilt'],
       zoom: json['zoom'],
     );
   }
 
   @override
-  bool operator ==(dynamic other) {
+  bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (runtimeType != other.runtimeType) return false;
-    final CameraPosition typedOther = other;
+    final CameraPosition typedOther = other as CameraPosition;
     return bearing == typedOther.bearing &&
         target == typedOther.target &&
         tilt == typedOther.tilt &&
@@ -112,14 +114,14 @@
   /// Returns a camera update that moves the camera to the specified position.
   static CameraUpdate newCameraPosition(CameraPosition cameraPosition) {
     return CameraUpdate._(
-      <dynamic>['newCameraPosition', cameraPosition.toMap()],
+      <Object>['newCameraPosition', cameraPosition.toMap()],
     );
   }
 
   /// Returns a camera update that moves the camera target to the specified
   /// geographical location.
   static CameraUpdate newLatLng(LatLng latLng) {
-    return CameraUpdate._(<dynamic>['newLatLng', latLng.toJson()]);
+    return CameraUpdate._(<Object>['newLatLng', latLng.toJson()]);
   }
 
   /// Returns a camera update that transforms the camera so that the specified
@@ -127,7 +129,7 @@
   /// possible zoom level. A non-zero [padding] insets the bounding box from the
   /// map view's edges. The camera's new tilt and bearing will both be 0.0.
   static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) {
-    return CameraUpdate._(<dynamic>[
+    return CameraUpdate._(<Object>[
       'newLatLngBounds',
       bounds.toJson(),
       padding,
@@ -138,7 +140,7 @@
   /// geographical location and zoom level.
   static CameraUpdate newLatLngZoom(LatLng latLng, double zoom) {
     return CameraUpdate._(
-      <dynamic>['newLatLngZoom', latLng.toJson(), zoom],
+      <Object>['newLatLngZoom', latLng.toJson(), zoom],
     );
   }
 
@@ -150,18 +152,18 @@
   /// 75 to the south of the current location, measured in screen coordinates.
   static CameraUpdate scrollBy(double dx, double dy) {
     return CameraUpdate._(
-      <dynamic>['scrollBy', dx, dy],
+      <Object>['scrollBy', dx, dy],
     );
   }
 
   /// Returns a camera update that modifies the camera zoom level by the
   /// specified amount. The optional [focus] is a screen point whose underlying
   /// geographical location should be invariant, if possible, by the movement.
-  static CameraUpdate zoomBy(double amount, [Offset focus]) {
+  static CameraUpdate zoomBy(double amount, [Offset? focus]) {
     if (focus == null) {
-      return CameraUpdate._(<dynamic>['zoomBy', amount]);
+      return CameraUpdate._(<Object>['zoomBy', amount]);
     } else {
-      return CameraUpdate._(<dynamic>[
+      return CameraUpdate._(<Object>[
         'zoomBy',
         amount,
         <double>[focus.dx, focus.dy],
@@ -174,7 +176,7 @@
   ///
   /// Equivalent to the result of calling `zoomBy(1.0)`.
   static CameraUpdate zoomIn() {
-    return CameraUpdate._(<dynamic>['zoomIn']);
+    return CameraUpdate._(<Object>['zoomIn']);
   }
 
   /// Returns a camera update that zooms the camera out, bringing the camera
@@ -182,16 +184,16 @@
   ///
   /// Equivalent to the result of calling `zoomBy(-1.0)`.
   static CameraUpdate zoomOut() {
-    return CameraUpdate._(<dynamic>['zoomOut']);
+    return CameraUpdate._(<Object>['zoomOut']);
   }
 
   /// Returns a camera update that sets the camera zoom level.
   static CameraUpdate zoomTo(double zoom) {
-    return CameraUpdate._(<dynamic>['zoomTo', zoom]);
+    return CameraUpdate._(<Object>['zoomTo', zoom]);
   }
 
-  final dynamic _json;
+  final Object _json;
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() => _json;
+  Object toJson() => _json;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart
index 68bf14c..c88923a 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart
@@ -17,16 +17,16 @@
   ///
   /// This is the default cap type at start and end vertices of Polylines with
   /// solid stroke pattern.
-  static const Cap buttCap = Cap._(<dynamic>['buttCap']);
+  static const Cap buttCap = Cap._(<Object>['buttCap']);
 
   /// Cap that is a semicircle with radius equal to half the stroke width,
   /// centered at the start or end vertex of a [Polyline] with solid stroke
   /// pattern.
-  static const Cap roundCap = Cap._(<dynamic>['roundCap']);
+  static const Cap roundCap = Cap._(<Object>['roundCap']);
 
   /// Cap that is squared off after extending half the stroke width beyond the
   /// start or end vertex of a [Polyline] with solid stroke pattern.
-  static const Cap squareCap = Cap._(<dynamic>['squareCap']);
+  static const Cap squareCap = Cap._(<Object>['squareCap']);
 
   /// Constructs a new CustomCap with a bitmap overlay centered at the start or
   /// end vertex of a [Polyline], orientated according to the direction of the line's
@@ -45,11 +45,11 @@
   }) {
     assert(bitmapDescriptor != null);
     assert(refWidth > 0.0);
-    return Cap._(<dynamic>['customCap', bitmapDescriptor.toJson(), refWidth]);
+    return Cap._(<Object>['customCap', bitmapDescriptor.toJson(), refWidth]);
   }
 
-  final dynamic _json;
+  final Object _json;
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() => _json;
+  Object toJson() => _json;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart
index d1418a4..e3198df 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart
@@ -4,7 +4,7 @@
 
 import 'package:flutter/foundation.dart' show VoidCallback;
 import 'package:flutter/material.dart' show Color, Colors;
-import 'package:meta/meta.dart' show immutable, required;
+import 'package:meta/meta.dart' show immutable;
 
 import 'types.dart';
 
@@ -12,36 +12,17 @@
 ///
 /// This does not have to be globally unique, only unique among the list.
 @immutable
-class CircleId {
+class CircleId extends MapsObjectId<Circle> {
   /// Creates an immutable identifier for a [Circle].
-  CircleId(this.value) : assert(value != null);
-
-  /// value of the [CircleId].
-  final String value;
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final CircleId typedOther = other;
-    return value == typedOther.value;
-  }
-
-  @override
-  int get hashCode => value.hashCode;
-
-  @override
-  String toString() {
-    return 'CircleId{value: $value}';
-  }
+  CircleId(String value) : super(value);
 }
 
 /// Draws a circle on the map.
 @immutable
-class Circle {
+class Circle implements MapsObject<Circle> {
   /// Creates an immutable representation of a [Circle] to draw on [GoogleMap].
   const Circle({
-    @required this.circleId,
+    required this.circleId,
     this.consumeTapEvents = false,
     this.fillColor = Colors.transparent,
     this.center = const LatLng(0.0, 0.0),
@@ -56,6 +37,9 @@
   /// Uniquely identifies a [Circle].
   final CircleId circleId;
 
+  @override
+  CircleId get mapsId => circleId;
+
   /// True if the [Circle] consumes tap events.
   ///
   /// If this is false, [onTap] callback will not be triggered.
@@ -91,20 +75,20 @@
   final int zIndex;
 
   /// Callbacks to receive tap events for circle placed on this map.
-  final VoidCallback onTap;
+  final VoidCallback? onTap;
 
   /// Creates a new [Circle] object whose values are the same as this instance,
   /// unless overwritten by the specified parameters.
   Circle copyWith({
-    bool consumeTapEventsParam,
-    Color fillColorParam,
-    LatLng centerParam,
-    double radiusParam,
-    Color strokeColorParam,
-    int strokeWidthParam,
-    bool visibleParam,
-    int zIndexParam,
-    VoidCallback onTapParam,
+    bool? consumeTapEventsParam,
+    Color? fillColorParam,
+    LatLng? centerParam,
+    double? radiusParam,
+    Color? strokeColorParam,
+    int? strokeWidthParam,
+    bool? visibleParam,
+    int? zIndexParam,
+    VoidCallback? onTapParam,
   }) {
     return Circle(
       circleId: circleId,
@@ -124,10 +108,10 @@
   Circle clone() => copyWith();
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  Object toJson() {
+    final Map<String, Object> json = <String, Object>{};
 
-    void addIfPresent(String fieldName, dynamic value) {
+    void addIfPresent(String fieldName, Object? value) {
       if (value != null) {
         json[fieldName] = value;
       }
@@ -150,7 +134,7 @@
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (other.runtimeType != runtimeType) return false;
-    final Circle typedOther = other;
+    final Circle typedOther = other as Circle;
     return circleId == typedOther.circleId &&
         consumeTapEvents == typedOther.consumeTapEvents &&
         fillColor == typedOther.fillColor &&
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart
index 6f49442..a0b064b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart
@@ -2,109 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:ui' show hashValues;
-
-import 'package:flutter/foundation.dart' show setEquals;
-
 import 'types.dart';
-import 'utils/circle.dart';
 
 /// [Circle] update events to be applied to the [GoogleMap].
 ///
 /// Used in [GoogleMapController] when the map is updated.
 // (Do not re-export)
-class CircleUpdates {
+class CircleUpdates extends MapsObjectUpdates<Circle> {
   /// Computes [CircleUpdates] given previous and current [Circle]s.
-  CircleUpdates.from(Set<Circle> previous, Set<Circle> current) {
-    if (previous == null) {
-      previous = Set<Circle>.identity();
-    }
-
-    if (current == null) {
-      current = Set<Circle>.identity();
-    }
-
-    final Map<CircleId, Circle> previousCircles = keyByCircleId(previous);
-    final Map<CircleId, Circle> currentCircles = keyByCircleId(current);
-
-    final Set<CircleId> prevCircleIds = previousCircles.keys.toSet();
-    final Set<CircleId> currentCircleIds = currentCircles.keys.toSet();
-
-    Circle idToCurrentCircle(CircleId id) {
-      return currentCircles[id];
-    }
-
-    final Set<CircleId> _circleIdsToRemove =
-        prevCircleIds.difference(currentCircleIds);
-
-    final Set<Circle> _circlesToAdd = currentCircleIds
-        .difference(prevCircleIds)
-        .map(idToCurrentCircle)
-        .toSet();
-
-    /// Returns `true` if [current] is not equals to previous one with the
-    /// same id.
-    bool hasChanged(Circle current) {
-      final Circle previous = previousCircles[current.circleId];
-      return current != previous;
-    }
-
-    final Set<Circle> _circlesToChange = currentCircleIds
-        .intersection(prevCircleIds)
-        .map(idToCurrentCircle)
-        .where(hasChanged)
-        .toSet();
-
-    circlesToAdd = _circlesToAdd;
-    circleIdsToRemove = _circleIdsToRemove;
-    circlesToChange = _circlesToChange;
-  }
+  CircleUpdates.from(Set<Circle> previous, Set<Circle> current)
+      : super.from(previous, current, objectName: 'circle');
 
   /// Set of Circles to be added in this update.
-  Set<Circle> circlesToAdd;
+  Set<Circle> get circlesToAdd => objectsToAdd;
 
   /// Set of CircleIds to be removed in this update.
-  Set<CircleId> circleIdsToRemove;
+  Set<CircleId> get circleIdsToRemove => objectIdsToRemove.cast<CircleId>();
 
   /// Set of Circles to be changed in this update.
-  Set<Circle> circlesToChange;
-
-  /// Converts this object to something serializable in JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> updateMap = <String, dynamic>{};
-
-    void addIfNonNull(String fieldName, dynamic value) {
-      if (value != null) {
-        updateMap[fieldName] = value;
-      }
-    }
-
-    addIfNonNull('circlesToAdd', serializeCircleSet(circlesToAdd));
-    addIfNonNull('circlesToChange', serializeCircleSet(circlesToChange));
-    addIfNonNull('circleIdsToRemove',
-        circleIdsToRemove.map<dynamic>((CircleId m) => m.value).toList());
-
-    return updateMap;
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final CircleUpdates typedOther = other;
-    return setEquals(circlesToAdd, typedOther.circlesToAdd) &&
-        setEquals(circleIdsToRemove, typedOther.circleIdsToRemove) &&
-        setEquals(circlesToChange, typedOther.circlesToChange);
-  }
-
-  @override
-  int get hashCode =>
-      hashValues(circlesToAdd, circleIdsToRemove, circlesToChange);
-
-  @override
-  String toString() {
-    return '_CircleUpdates{circlesToAdd: $circlesToAdd, '
-        'circleIdsToRemove: $circleIdsToRemove, '
-        'circlesToChange: $circlesToChange}';
-  }
+  Set<Circle> get circlesToChange => objectsToChange;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart
index 6b76a6d..a719f0b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart
@@ -29,16 +29,18 @@
   final double longitude;
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() {
+  Object toJson() {
     return <double>[latitude, longitude];
   }
 
   /// Initialize a LatLng from an \[lat, lng\] array.
-  static LatLng fromJson(dynamic json) {
+  static LatLng? fromJson(Object? json) {
     if (json == null) {
       return null;
     }
-    return LatLng(json[0], json[1]);
+    assert(json is List && json.length == 2);
+    final list = json as List;
+    return LatLng(list[0], list[1]);
   }
 
   @override
@@ -66,7 +68,7 @@
   ///
   /// The latitude of the southwest corner cannot be larger than the
   /// latitude of the northeast corner.
-  LatLngBounds({@required this.southwest, @required this.northeast})
+  LatLngBounds({required this.southwest, required this.northeast})
       : assert(southwest != null),
         assert(northeast != null),
         assert(southwest.latitude <= northeast.latitude);
@@ -78,8 +80,8 @@
   final LatLng northeast;
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() {
-    return <dynamic>[southwest.toJson(), northeast.toJson()];
+  Object toJson() {
+    return <Object>[southwest.toJson(), northeast.toJson()];
   }
 
   /// Returns whether this rectangle contains the given [LatLng].
@@ -102,13 +104,15 @@
 
   /// Converts a list to [LatLngBounds].
   @visibleForTesting
-  static LatLngBounds fromList(dynamic json) {
+  static LatLngBounds? fromList(Object? json) {
     if (json == null) {
       return null;
     }
+    assert(json is List && json.length == 2);
+    final list = json as List;
     return LatLngBounds(
-      southwest: LatLng.fromJson(json[0]),
-      northeast: LatLng.fromJson(json[1]),
+      southwest: LatLng.fromJson(list[0])!,
+      northeast: LatLng.fromJson(list[1])!,
     );
   }
 
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart
new file mode 100644
index 0000000..545d462
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart
@@ -0,0 +1,49 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart' show objectRuntimeType;
+import 'package:meta/meta.dart' show immutable;
+
+/// Uniquely identifies object an among [GoogleMap] collections of a specific
+/// type.
+///
+/// This does not have to be globally unique, only unique among the collection.
+@immutable
+class MapsObjectId<T> {
+  /// Creates an immutable object representing a [T] among [GoogleMap] Ts.
+  ///
+  /// An [AssertionError] will be thrown if [value] is null.
+  MapsObjectId(this.value) : assert(value != null);
+
+  /// The value of the id.
+  final String value;
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    if (other.runtimeType != runtimeType) return false;
+    final MapsObjectId<T> typedOther = other as MapsObjectId<T>;
+    return value == typedOther.value;
+  }
+
+  @override
+  int get hashCode => value.hashCode;
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'MapsObjectId')}($value)';
+  }
+}
+
+/// A common interface for maps types.
+abstract class MapsObject<T> {
+  /// A identifier for this object.
+  MapsObjectId<T> get mapsId;
+
+  /// Returns a duplicate of this object.
+  T clone();
+
+  /// Converts this object to something serializable in JSON.
+  Object toJson();
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart
new file mode 100644
index 0000000..01cf967
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart
@@ -0,0 +1,126 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui' show hashValues, hashList;
+
+import 'package:flutter/foundation.dart' show objectRuntimeType, setEquals;
+
+import 'maps_object.dart';
+import 'utils/maps_object.dart';
+
+/// Update specification for a set of objects.
+class MapsObjectUpdates<T extends MapsObject> {
+  /// Computes updates given previous and current object sets.
+  ///
+  /// [objectName] is the prefix to use when serializing the updates into a JSON
+  /// dictionary. E.g., 'circle' will give 'circlesToAdd', 'circlesToUpdate',
+  /// 'circleIdsToRemove'.
+  MapsObjectUpdates.from(
+    Set<T> previous,
+    Set<T> current, {
+    required this.objectName,
+  }) {
+    final Map<MapsObjectId<T>, T> previousObjects = keyByMapsObjectId(previous);
+    final Map<MapsObjectId<T>, T> currentObjects = keyByMapsObjectId(current);
+
+    final Set<MapsObjectId<T>> previousObjectIds = previousObjects.keys.toSet();
+    final Set<MapsObjectId<T>> currentObjectIds = currentObjects.keys.toSet();
+
+    /// Maps an ID back to a [T] in [currentObjects].
+    ///
+    /// It is a programming error to call this with an ID that is not guaranteed
+    /// to be in [currentObjects].
+    T _idToCurrentObject(MapsObjectId<T> id) {
+      return currentObjects[id]!;
+    }
+
+    _objectIdsToRemove = previousObjectIds.difference(currentObjectIds);
+
+    _objectsToAdd = currentObjectIds
+        .difference(previousObjectIds)
+        .map(_idToCurrentObject)
+        .toSet();
+
+    // Returns `true` if [current] is not equals to previous one with the
+    // same id.
+    bool hasChanged(T current) {
+      final T? previous = previousObjects[current.mapsId as MapsObjectId<T>];
+      return current != previous;
+    }
+
+    _objectsToChange = currentObjectIds
+        .intersection(previousObjectIds)
+        .map(_idToCurrentObject)
+        .where(hasChanged)
+        .toSet();
+  }
+
+  /// The name of the objects being updated, for use in serialization.
+  final String objectName;
+
+  /// Set of objects to be added in this update.
+  Set<T> get objectsToAdd {
+    return _objectsToAdd;
+  }
+
+  late Set<T> _objectsToAdd;
+
+  /// Set of objects to be removed in this update.
+  Set<MapsObjectId<T>> get objectIdsToRemove {
+    return _objectIdsToRemove;
+  }
+
+  late Set<MapsObjectId<T>> _objectIdsToRemove;
+
+  /// Set of objects to be changed in this update.
+  Set<T> get objectsToChange {
+    return _objectsToChange;
+  }
+
+  late Set<T> _objectsToChange;
+
+  /// Converts this object to JSON.
+  Object toJson() {
+    final Map<String, Object> updateMap = <String, Object>{};
+
+    void addIfNonNull(String fieldName, Object? value) {
+      if (value != null) {
+        updateMap[fieldName] = value;
+      }
+    }
+
+    addIfNonNull('${objectName}sToAdd', serializeMapsObjectSet(_objectsToAdd));
+    addIfNonNull(
+        '${objectName}sToChange', serializeMapsObjectSet(_objectsToChange));
+    addIfNonNull(
+        '${objectName}IdsToRemove',
+        _objectIdsToRemove
+            .map<String>((MapsObjectId<T> m) => m.value)
+            .toList());
+
+    return updateMap;
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is MapsObjectUpdates &&
+        setEquals(_objectsToAdd, other._objectsToAdd) &&
+        setEquals(_objectIdsToRemove, other._objectIdsToRemove) &&
+        setEquals(_objectsToChange, other._objectsToChange);
+  }
+
+  @override
+  int get hashCode => hashValues(hashList(_objectsToAdd),
+      hashList(_objectIdsToRemove), hashList(_objectsToChange));
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'MapsObjectUpdates')}(add: $objectsToAdd, '
+        'remove: $objectIdsToRemove, '
+        'change: $objectsToChange)';
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart
index 9b57f96..15351d5 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart
@@ -5,15 +5,12 @@
 import 'dart:ui' show hashValues, Offset;
 
 import 'package:flutter/foundation.dart' show ValueChanged, VoidCallback;
-import 'package:meta/meta.dart' show immutable, required;
+import 'package:meta/meta.dart' show immutable;
 
 import 'types.dart';
 
-dynamic _offsetToJson(Offset offset) {
-  if (offset == null) {
-    return null;
-  }
-  return <dynamic>[offset.dx, offset.dy];
+Object _offsetToJson(Offset offset) {
+  return <Object>[offset.dx, offset.dy];
 }
 
 /// Text labels for a [Marker] info window.
@@ -32,12 +29,12 @@
   /// Text displayed in an info window when the user taps the marker.
   ///
   /// A null value means no title.
-  final String title;
+  final String? title;
 
   /// Additional text displayed below the [title].
   ///
   /// A null value means no additional text.
-  final String snippet;
+  final String? snippet;
 
   /// The icon image point that will be the anchor of the info window when
   /// displayed.
@@ -48,15 +45,15 @@
   final Offset anchor;
 
   /// onTap callback for this [InfoWindow].
-  final VoidCallback onTap;
+  final VoidCallback? onTap;
 
   /// Creates a new [InfoWindow] object whose values are the same as this instance,
   /// unless overwritten by the specified parameters.
   InfoWindow copyWith({
-    String titleParam,
-    String snippetParam,
-    Offset anchorParam,
-    VoidCallback onTapParam,
+    String? titleParam,
+    String? snippetParam,
+    Offset? anchorParam,
+    VoidCallback? onTapParam,
   }) {
     return InfoWindow(
       title: titleParam ?? title,
@@ -66,10 +63,10 @@
     );
   }
 
-  dynamic _toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  Object _toJson() {
+    final Map<String, Object> json = <String, Object>{};
 
-    void addIfPresent(String fieldName, dynamic value) {
+    void addIfPresent(String fieldName, Object? value) {
       if (value != null) {
         json[fieldName] = value;
       }
@@ -86,7 +83,7 @@
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (other.runtimeType != runtimeType) return false;
-    final InfoWindow typedOther = other;
+    final InfoWindow typedOther = other as InfoWindow;
     return title == typedOther.title &&
         snippet == typedOther.snippet &&
         anchor == typedOther.anchor;
@@ -105,28 +102,9 @@
 ///
 /// This does not have to be globally unique, only unique among the list.
 @immutable
-class MarkerId {
+class MarkerId extends MapsObjectId<Marker> {
   /// Creates an immutable identifier for a [Marker].
-  MarkerId(this.value) : assert(value != null);
-
-  /// value of the [MarkerId].
-  final String value;
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final MarkerId typedOther = other;
-    return value == typedOther.value;
-  }
-
-  @override
-  int get hashCode => value.hashCode;
-
-  @override
-  String toString() {
-    return 'MarkerId{value: $value}';
-  }
+  MarkerId(String value) : super(value);
 }
 
 /// Marks a geographical location on the map.
@@ -135,7 +113,7 @@
 /// the map's surface; that is, it will not necessarily change orientation
 /// due to map rotations, tilting, or zooming.
 @immutable
-class Marker {
+class Marker implements MapsObject {
   /// Creates a set of marker configuration options.
   ///
   /// Default marker options.
@@ -156,7 +134,7 @@
   /// * reports [onTap] events
   /// * reports [onDragEnd] events
   const Marker({
-    @required this.markerId,
+    required this.markerId,
     this.alpha = 1.0,
     this.anchor = const Offset(0.5, 1.0),
     this.consumeTapEvents = false,
@@ -175,6 +153,9 @@
   /// Uniquely identifies a [Marker].
   final MarkerId markerId;
 
+  @override
+  MarkerId get mapsId => markerId;
+
   /// The opacity of the marker, between 0.0 and 1.0 inclusive.
   ///
   /// 0.0 means fully transparent, 1.0 means fully opaque.
@@ -224,27 +205,27 @@
   final double zIndex;
 
   /// Callbacks to receive tap events for markers placed on this map.
-  final VoidCallback onTap;
+  final VoidCallback? onTap;
 
   /// Signature reporting the new [LatLng] at the end of a drag event.
-  final ValueChanged<LatLng> onDragEnd;
+  final ValueChanged<LatLng>? onDragEnd;
 
   /// Creates a new [Marker] object whose values are the same as this instance,
   /// unless overwritten by the specified parameters.
   Marker copyWith({
-    double alphaParam,
-    Offset anchorParam,
-    bool consumeTapEventsParam,
-    bool draggableParam,
-    bool flatParam,
-    BitmapDescriptor iconParam,
-    InfoWindow infoWindowParam,
-    LatLng positionParam,
-    double rotationParam,
-    bool visibleParam,
-    double zIndexParam,
-    VoidCallback onTapParam,
-    ValueChanged<LatLng> onDragEndParam,
+    double? alphaParam,
+    Offset? anchorParam,
+    bool? consumeTapEventsParam,
+    bool? draggableParam,
+    bool? flatParam,
+    BitmapDescriptor? iconParam,
+    InfoWindow? infoWindowParam,
+    LatLng? positionParam,
+    double? rotationParam,
+    bool? visibleParam,
+    double? zIndexParam,
+    VoidCallback? onTapParam,
+    ValueChanged<LatLng>? onDragEndParam,
   }) {
     return Marker(
       markerId: markerId,
@@ -268,10 +249,10 @@
   Marker clone() => copyWith();
 
   /// Converts this object to something serializable in JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  Object toJson() {
+    final Map<String, Object> json = <String, Object>{};
 
-    void addIfPresent(String fieldName, dynamic value) {
+    void addIfPresent(String fieldName, Object? value) {
       if (value != null) {
         json[fieldName] = value;
       }
@@ -283,9 +264,9 @@
     addIfPresent('consumeTapEvents', consumeTapEvents);
     addIfPresent('draggable', draggable);
     addIfPresent('flat', flat);
-    addIfPresent('icon', icon?.toJson());
-    addIfPresent('infoWindow', infoWindow?._toJson());
-    addIfPresent('position', position?.toJson());
+    addIfPresent('icon', icon.toJson());
+    addIfPresent('infoWindow', infoWindow._toJson());
+    addIfPresent('position', position.toJson());
     addIfPresent('rotation', rotation);
     addIfPresent('visible', visible);
     addIfPresent('zIndex', zIndex);
@@ -296,7 +277,7 @@
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (other.runtimeType != runtimeType) return false;
-    final Marker typedOther = other;
+    final Marker typedOther = other as Marker;
     return markerId == typedOther.markerId &&
         alpha == typedOther.alpha &&
         anchor == typedOther.anchor &&
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart
index bb6ea88..9c96ab6 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart
@@ -2,109 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:ui' show hashValues;
-
-import 'package:flutter/foundation.dart' show setEquals;
-
 import 'types.dart';
-import 'utils/marker.dart';
 
 /// [Marker] update events to be applied to the [GoogleMap].
 ///
 /// Used in [GoogleMapController] when the map is updated.
 // (Do not re-export)
-class MarkerUpdates {
+class MarkerUpdates extends MapsObjectUpdates<Marker> {
   /// Computes [MarkerUpdates] given previous and current [Marker]s.
-  MarkerUpdates.from(Set<Marker> previous, Set<Marker> current) {
-    if (previous == null) {
-      previous = Set<Marker>.identity();
-    }
-
-    if (current == null) {
-      current = Set<Marker>.identity();
-    }
-
-    final Map<MarkerId, Marker> previousMarkers = keyByMarkerId(previous);
-    final Map<MarkerId, Marker> currentMarkers = keyByMarkerId(current);
-
-    final Set<MarkerId> prevMarkerIds = previousMarkers.keys.toSet();
-    final Set<MarkerId> currentMarkerIds = currentMarkers.keys.toSet();
-
-    Marker idToCurrentMarker(MarkerId id) {
-      return currentMarkers[id];
-    }
-
-    final Set<MarkerId> _markerIdsToRemove =
-        prevMarkerIds.difference(currentMarkerIds);
-
-    final Set<Marker> _markersToAdd = currentMarkerIds
-        .difference(prevMarkerIds)
-        .map(idToCurrentMarker)
-        .toSet();
-
-    /// Returns `true` if [current] is not equals to previous one with the
-    /// same id.
-    bool hasChanged(Marker current) {
-      final Marker previous = previousMarkers[current.markerId];
-      return current != previous;
-    }
-
-    final Set<Marker> _markersToChange = currentMarkerIds
-        .intersection(prevMarkerIds)
-        .map(idToCurrentMarker)
-        .where(hasChanged)
-        .toSet();
-
-    markersToAdd = _markersToAdd;
-    markerIdsToRemove = _markerIdsToRemove;
-    markersToChange = _markersToChange;
-  }
+  MarkerUpdates.from(Set<Marker> previous, Set<Marker> current)
+      : super.from(previous, current, objectName: 'marker');
 
   /// Set of Markers to be added in this update.
-  Set<Marker> markersToAdd;
+  Set<Marker> get markersToAdd => objectsToAdd;
 
   /// Set of MarkerIds to be removed in this update.
-  Set<MarkerId> markerIdsToRemove;
+  Set<MarkerId> get markerIdsToRemove => objectIdsToRemove.cast<MarkerId>();
 
   /// Set of Markers to be changed in this update.
-  Set<Marker> markersToChange;
-
-  /// Converts this object to something serializable in JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> updateMap = <String, dynamic>{};
-
-    void addIfNonNull(String fieldName, dynamic value) {
-      if (value != null) {
-        updateMap[fieldName] = value;
-      }
-    }
-
-    addIfNonNull('markersToAdd', serializeMarkerSet(markersToAdd));
-    addIfNonNull('markersToChange', serializeMarkerSet(markersToChange));
-    addIfNonNull('markerIdsToRemove',
-        markerIdsToRemove.map<dynamic>((MarkerId m) => m.value).toList());
-
-    return updateMap;
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final MarkerUpdates typedOther = other;
-    return setEquals(markersToAdd, typedOther.markersToAdd) &&
-        setEquals(markerIdsToRemove, typedOther.markerIdsToRemove) &&
-        setEquals(markersToChange, typedOther.markersToChange);
-  }
-
-  @override
-  int get hashCode =>
-      hashValues(markersToAdd, markerIdsToRemove, markersToChange);
-
-  @override
-  String toString() {
-    return '_MarkerUpdates{markersToAdd: $markersToAdd, '
-        'markerIdsToRemove: $markerIdsToRemove, '
-        'markersToChange: $markersToChange}';
-  }
+  Set<Marker> get markersToChange => objectsToChange;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart
index 28c7ce9..f1cd7f4 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart
@@ -10,14 +10,14 @@
   const PatternItem._(this._json);
 
   /// A dot used in the stroke pattern for a [Polyline].
-  static const PatternItem dot = PatternItem._(<dynamic>['dot']);
+  static const PatternItem dot = PatternItem._(<Object>['dot']);
 
   /// A dash used in the stroke pattern for a [Polyline].
   ///
   /// [length] has to be non-negative.
   static PatternItem dash(double length) {
     assert(length >= 0.0);
-    return PatternItem._(<dynamic>['dash', length]);
+    return PatternItem._(<Object>['dash', length]);
   }
 
   /// A gap used in the stroke pattern for a [Polyline].
@@ -25,11 +25,11 @@
   /// [length] has to be non-negative.
   static PatternItem gap(double length) {
     assert(length >= 0.0);
-    return PatternItem._(<dynamic>['gap', length]);
+    return PatternItem._(<Object>['gap', length]);
   }
 
-  final dynamic _json;
+  final Object _json;
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() => _json;
+  Object toJson() => _json;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart
index 96b3915..4e5e9bf 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart
@@ -5,7 +5,7 @@
 import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart' show listEquals, VoidCallback;
 import 'package:flutter/material.dart' show Color, Colors;
-import 'package:meta/meta.dart' show immutable, required;
+import 'package:meta/meta.dart' show immutable;
 
 import 'types.dart';
 
@@ -13,36 +13,17 @@
 ///
 /// This does not have to be globally unique, only unique among the list.
 @immutable
-class PolygonId {
+class PolygonId extends MapsObjectId<Polygon> {
   /// Creates an immutable identifier for a [Polygon].
-  PolygonId(this.value) : assert(value != null);
-
-  /// value of the [PolygonId].
-  final String value;
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final PolygonId typedOther = other;
-    return value == typedOther.value;
-  }
-
-  @override
-  int get hashCode => value.hashCode;
-
-  @override
-  String toString() {
-    return 'PolygonId{value: $value}';
-  }
+  PolygonId(String value) : super(value);
 }
 
 /// Draws a polygon through geographical locations on the map.
 @immutable
-class Polygon {
+class Polygon implements MapsObject {
   /// Creates an immutable representation of a polygon through geographical locations on the map.
   const Polygon({
-    @required this.polygonId,
+    required this.polygonId,
     this.consumeTapEvents = false,
     this.fillColor = Colors.black,
     this.geodesic = false,
@@ -58,6 +39,9 @@
   /// Uniquely identifies a [Polygon].
   final PolygonId polygonId;
 
+  @override
+  PolygonId get mapsId => polygonId;
+
   /// True if the [Polygon] consumes tap events.
   ///
   /// If this is false, [onTap] callback will not be triggered.
@@ -107,21 +91,21 @@
   final int zIndex;
 
   /// Callbacks to receive tap events for polygon placed on this map.
-  final VoidCallback onTap;
+  final VoidCallback? onTap;
 
   /// Creates a new [Polygon] object whose values are the same as this instance,
   /// unless overwritten by the specified parameters.
   Polygon copyWith({
-    bool consumeTapEventsParam,
-    Color fillColorParam,
-    bool geodesicParam,
-    List<LatLng> pointsParam,
-    List<List<LatLng>> holesParam,
-    Color strokeColorParam,
-    int strokeWidthParam,
-    bool visibleParam,
-    int zIndexParam,
-    VoidCallback onTapParam,
+    bool? consumeTapEventsParam,
+    Color? fillColorParam,
+    bool? geodesicParam,
+    List<LatLng>? pointsParam,
+    List<List<LatLng>>? holesParam,
+    Color? strokeColorParam,
+    int? strokeWidthParam,
+    bool? visibleParam,
+    int? zIndexParam,
+    VoidCallback? onTapParam,
   }) {
     return Polygon(
       polygonId: polygonId,
@@ -144,10 +128,10 @@
   }
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  Object toJson() {
+    final Map<String, Object> json = <String, Object>{};
 
-    void addIfPresent(String fieldName, dynamic value) {
+    void addIfPresent(String fieldName, Object? value) {
       if (value != null) {
         json[fieldName] = value;
       }
@@ -177,7 +161,7 @@
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (other.runtimeType != runtimeType) return false;
-    final Polygon typedOther = other;
+    final Polygon typedOther = other as Polygon;
     return polygonId == typedOther.polygonId &&
         consumeTapEvents == typedOther.consumeTapEvents &&
         fillColor == typedOther.fillColor &&
@@ -193,18 +177,18 @@
   @override
   int get hashCode => polygonId.hashCode;
 
-  dynamic _pointsToJson() {
-    final List<dynamic> result = <dynamic>[];
+  Object _pointsToJson() {
+    final List<Object> result = <Object>[];
     for (final LatLng point in points) {
       result.add(point.toJson());
     }
     return result;
   }
 
-  List<List<dynamic>> _holesToJson() {
-    final List<List<dynamic>> result = <List<dynamic>>[];
+  List<List<Object>> _holesToJson() {
+    final List<List<Object>> result = <List<Object>>[];
     for (final List<LatLng> hole in holes) {
-      final List<dynamic> jsonHole = <dynamic>[];
+      final List<Object> jsonHole = <Object>[];
       for (final LatLng point in hole) {
         jsonHole.add(point.toJson());
       }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart
index cc8b8e2..29b74ae 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart
@@ -2,109 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:ui' show hashValues;
-
-import 'package:flutter/foundation.dart' show setEquals;
-
 import 'types.dart';
-import 'utils/polygon.dart';
 
 /// [Polygon] update events to be applied to the [GoogleMap].
 ///
 /// Used in [GoogleMapController] when the map is updated.
 // (Do not re-export)
-class PolygonUpdates {
+class PolygonUpdates extends MapsObjectUpdates<Polygon> {
   /// Computes [PolygonUpdates] given previous and current [Polygon]s.
-  PolygonUpdates.from(Set<Polygon> previous, Set<Polygon> current) {
-    if (previous == null) {
-      previous = Set<Polygon>.identity();
-    }
-
-    if (current == null) {
-      current = Set<Polygon>.identity();
-    }
-
-    final Map<PolygonId, Polygon> previousPolygons = keyByPolygonId(previous);
-    final Map<PolygonId, Polygon> currentPolygons = keyByPolygonId(current);
-
-    final Set<PolygonId> prevPolygonIds = previousPolygons.keys.toSet();
-    final Set<PolygonId> currentPolygonIds = currentPolygons.keys.toSet();
-
-    Polygon idToCurrentPolygon(PolygonId id) {
-      return currentPolygons[id];
-    }
-
-    final Set<PolygonId> _polygonIdsToRemove =
-        prevPolygonIds.difference(currentPolygonIds);
-
-    final Set<Polygon> _polygonsToAdd = currentPolygonIds
-        .difference(prevPolygonIds)
-        .map(idToCurrentPolygon)
-        .toSet();
-
-    /// Returns `true` if [current] is not equals to previous one with the
-    /// same id.
-    bool hasChanged(Polygon current) {
-      final Polygon previous = previousPolygons[current.polygonId];
-      return current != previous;
-    }
-
-    final Set<Polygon> _polygonsToChange = currentPolygonIds
-        .intersection(prevPolygonIds)
-        .map(idToCurrentPolygon)
-        .where(hasChanged)
-        .toSet();
-
-    polygonsToAdd = _polygonsToAdd;
-    polygonIdsToRemove = _polygonIdsToRemove;
-    polygonsToChange = _polygonsToChange;
-  }
+  PolygonUpdates.from(Set<Polygon> previous, Set<Polygon> current)
+      : super.from(previous, current, objectName: 'polygon');
 
   /// Set of Polygons to be added in this update.
-  Set<Polygon> polygonsToAdd;
+  Set<Polygon> get polygonsToAdd => objectsToAdd;
 
   /// Set of PolygonIds to be removed in this update.
-  Set<PolygonId> polygonIdsToRemove;
+  Set<PolygonId> get polygonIdsToRemove => objectIdsToRemove.cast<PolygonId>();
 
   /// Set of Polygons to be changed in this update.
-  Set<Polygon> polygonsToChange;
-
-  /// Converts this object to something serializable in JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> updateMap = <String, dynamic>{};
-
-    void addIfNonNull(String fieldName, dynamic value) {
-      if (value != null) {
-        updateMap[fieldName] = value;
-      }
-    }
-
-    addIfNonNull('polygonsToAdd', serializePolygonSet(polygonsToAdd));
-    addIfNonNull('polygonsToChange', serializePolygonSet(polygonsToChange));
-    addIfNonNull('polygonIdsToRemove',
-        polygonIdsToRemove.map<dynamic>((PolygonId m) => m.value).toList());
-
-    return updateMap;
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final PolygonUpdates typedOther = other;
-    return setEquals(polygonsToAdd, typedOther.polygonsToAdd) &&
-        setEquals(polygonIdsToRemove, typedOther.polygonIdsToRemove) &&
-        setEquals(polygonsToChange, typedOther.polygonsToChange);
-  }
-
-  @override
-  int get hashCode =>
-      hashValues(polygonsToAdd, polygonIdsToRemove, polygonsToChange);
-
-  @override
-  String toString() {
-    return '_PolygonUpdates{polygonsToAdd: $polygonsToAdd, '
-        'polygonIdsToRemove: $polygonIdsToRemove, '
-        'polygonsToChange: $polygonsToChange}';
-  }
+  Set<Polygon> get polygonsToChange => objectsToChange;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart
index ae5c3b9..3f87395 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart
@@ -4,7 +4,7 @@
 
 import 'package:flutter/foundation.dart' show listEquals, VoidCallback;
 import 'package:flutter/material.dart' show Color, Colors;
-import 'package:meta/meta.dart' show immutable, required;
+import 'package:meta/meta.dart' show immutable;
 
 import 'types.dart';
 
@@ -12,38 +12,19 @@
 ///
 /// This does not have to be globally unique, only unique among the list.
 @immutable
-class PolylineId {
+class PolylineId extends MapsObjectId<Polyline> {
   /// Creates an immutable object representing a [PolylineId] among [GoogleMap] polylines.
   ///
   /// An [AssertionError] will be thrown if [value] is null.
-  PolylineId(this.value) : assert(value != null);
-
-  /// value of the [PolylineId].
-  final String value;
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final PolylineId typedOther = other;
-    return value == typedOther.value;
-  }
-
-  @override
-  int get hashCode => value.hashCode;
-
-  @override
-  String toString() {
-    return 'PolylineId{value: $value}';
-  }
+  PolylineId(String value) : super(value);
 }
 
 /// Draws a line through geographical locations on the map.
 @immutable
-class Polyline {
+class Polyline implements MapsObject {
   /// Creates an immutable object representing a line drawn through geographical locations on the map.
   const Polyline({
-    @required this.polylineId,
+    required this.polylineId,
     this.consumeTapEvents = false,
     this.color = Colors.black,
     this.endCap = Cap.buttCap,
@@ -61,6 +42,9 @@
   /// Uniquely identifies a [Polyline].
   final PolylineId polylineId;
 
+  @override
+  PolylineId get mapsId => polylineId;
+
   /// True if the [Polyline] consumes tap events.
   ///
   /// If this is false, [onTap] callback will not be triggered.
@@ -129,23 +113,23 @@
   final int zIndex;
 
   /// Callbacks to receive tap events for polyline placed on this map.
-  final VoidCallback onTap;
+  final VoidCallback? onTap;
 
   /// Creates a new [Polyline] object whose values are the same as this instance,
   /// unless overwritten by the specified parameters.
   Polyline copyWith({
-    Color colorParam,
-    bool consumeTapEventsParam,
-    Cap endCapParam,
-    bool geodesicParam,
-    JointType jointTypeParam,
-    List<PatternItem> patternsParam,
-    List<LatLng> pointsParam,
-    Cap startCapParam,
-    bool visibleParam,
-    int widthParam,
-    int zIndexParam,
-    VoidCallback onTapParam,
+    Color? colorParam,
+    bool? consumeTapEventsParam,
+    Cap? endCapParam,
+    bool? geodesicParam,
+    JointType? jointTypeParam,
+    List<PatternItem>? patternsParam,
+    List<LatLng>? pointsParam,
+    Cap? startCapParam,
+    bool? visibleParam,
+    int? widthParam,
+    int? zIndexParam,
+    VoidCallback? onTapParam,
   }) {
     return Polyline(
       polylineId: polylineId,
@@ -174,10 +158,10 @@
   }
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  Object toJson() {
+    final Map<String, Object> json = <String, Object>{};
 
-    void addIfPresent(String fieldName, dynamic value) {
+    void addIfPresent(String fieldName, Object? value) {
       if (value != null) {
         json[fieldName] = value;
       }
@@ -186,10 +170,10 @@
     addIfPresent('polylineId', polylineId.value);
     addIfPresent('consumeTapEvents', consumeTapEvents);
     addIfPresent('color', color.value);
-    addIfPresent('endCap', endCap?.toJson());
+    addIfPresent('endCap', endCap.toJson());
     addIfPresent('geodesic', geodesic);
-    addIfPresent('jointType', jointType?.value);
-    addIfPresent('startCap', startCap?.toJson());
+    addIfPresent('jointType', jointType.value);
+    addIfPresent('startCap', startCap.toJson());
     addIfPresent('visible', visible);
     addIfPresent('width', width);
     addIfPresent('zIndex', zIndex);
@@ -209,7 +193,7 @@
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (other.runtimeType != runtimeType) return false;
-    final Polyline typedOther = other;
+    final Polyline typedOther = other as Polyline;
     return polylineId == typedOther.polylineId &&
         consumeTapEvents == typedOther.consumeTapEvents &&
         color == typedOther.color &&
@@ -227,16 +211,16 @@
   @override
   int get hashCode => polylineId.hashCode;
 
-  dynamic _pointsToJson() {
-    final List<dynamic> result = <dynamic>[];
+  Object _pointsToJson() {
+    final List<Object> result = <Object>[];
     for (final LatLng point in points) {
       result.add(point.toJson());
     }
     return result;
   }
 
-  dynamic _patternToJson() {
-    final List<dynamic> result = <dynamic>[];
+  Object _patternToJson() {
+    final List<Object> result = <Object>[];
     for (final PatternItem patternItem in patterns) {
       if (patternItem != null) {
         result.add(patternItem.toJson());
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart
index f871928..60e0bfe 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart
@@ -2,110 +2,24 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:ui' show hashValues;
-
-import 'package:flutter/foundation.dart' show setEquals;
-
-import 'utils/polyline.dart';
 import 'types.dart';
 
 /// [Polyline] update events to be applied to the [GoogleMap].
 ///
 /// Used in [GoogleMapController] when the map is updated.
 // (Do not re-export)
-class PolylineUpdates {
+class PolylineUpdates extends MapsObjectUpdates<Polyline> {
   /// Computes [PolylineUpdates] given previous and current [Polyline]s.
-  PolylineUpdates.from(Set<Polyline> previous, Set<Polyline> current) {
-    if (previous == null) {
-      previous = Set<Polyline>.identity();
-    }
-
-    if (current == null) {
-      current = Set<Polyline>.identity();
-    }
-
-    final Map<PolylineId, Polyline> previousPolylines =
-        keyByPolylineId(previous);
-    final Map<PolylineId, Polyline> currentPolylines = keyByPolylineId(current);
-
-    final Set<PolylineId> prevPolylineIds = previousPolylines.keys.toSet();
-    final Set<PolylineId> currentPolylineIds = currentPolylines.keys.toSet();
-
-    Polyline idToCurrentPolyline(PolylineId id) {
-      return currentPolylines[id];
-    }
-
-    final Set<PolylineId> _polylineIdsToRemove =
-        prevPolylineIds.difference(currentPolylineIds);
-
-    final Set<Polyline> _polylinesToAdd = currentPolylineIds
-        .difference(prevPolylineIds)
-        .map(idToCurrentPolyline)
-        .toSet();
-
-    /// Returns `true` if [current] is not equals to previous one with the
-    /// same id.
-    bool hasChanged(Polyline current) {
-      final Polyline previous = previousPolylines[current.polylineId];
-      return current != previous;
-    }
-
-    final Set<Polyline> _polylinesToChange = currentPolylineIds
-        .intersection(prevPolylineIds)
-        .map(idToCurrentPolyline)
-        .where(hasChanged)
-        .toSet();
-
-    polylinesToAdd = _polylinesToAdd;
-    polylineIdsToRemove = _polylineIdsToRemove;
-    polylinesToChange = _polylinesToChange;
-  }
+  PolylineUpdates.from(Set<Polyline> previous, Set<Polyline> current)
+      : super.from(previous, current, objectName: 'polyline');
 
   /// Set of Polylines to be added in this update.
-  Set<Polyline> polylinesToAdd;
+  Set<Polyline> get polylinesToAdd => objectsToAdd;
 
   /// Set of PolylineIds to be removed in this update.
-  Set<PolylineId> polylineIdsToRemove;
+  Set<PolylineId> get polylineIdsToRemove =>
+      objectIdsToRemove.cast<PolylineId>();
 
   /// Set of Polylines to be changed in this update.
-  Set<Polyline> polylinesToChange;
-
-  /// Converts this object to something serializable in JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> updateMap = <String, dynamic>{};
-
-    void addIfNonNull(String fieldName, dynamic value) {
-      if (value != null) {
-        updateMap[fieldName] = value;
-      }
-    }
-
-    addIfNonNull('polylinesToAdd', serializePolylineSet(polylinesToAdd));
-    addIfNonNull('polylinesToChange', serializePolylineSet(polylinesToChange));
-    addIfNonNull('polylineIdsToRemove',
-        polylineIdsToRemove.map<dynamic>((PolylineId m) => m.value).toList());
-
-    return updateMap;
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-    if (other.runtimeType != runtimeType) return false;
-    final PolylineUpdates typedOther = other;
-    return setEquals(polylinesToAdd, typedOther.polylinesToAdd) &&
-        setEquals(polylineIdsToRemove, typedOther.polylineIdsToRemove) &&
-        setEquals(polylinesToChange, typedOther.polylinesToChange);
-  }
-
-  @override
-  int get hashCode =>
-      hashValues(polylinesToAdd, polylineIdsToRemove, polylinesToChange);
-
-  @override
-  String toString() {
-    return '_PolylineUpdates{polylinesToAdd: $polylinesToAdd, '
-        'polylineIdsToRemove: $polylineIdsToRemove, '
-        'polylinesToChange: $polylinesToChange}';
-  }
+  Set<Polyline> get polylinesToChange => objectsToChange;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart
index 965db79..af7a951 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart
@@ -4,7 +4,7 @@
 
 import 'dart:ui' show hashValues;
 
-import 'package:meta/meta.dart' show immutable, required;
+import 'package:meta/meta.dart' show immutable;
 
 /// Represents a point coordinate in the [GoogleMap]'s view.
 ///
@@ -15,8 +15,8 @@
 class ScreenCoordinate {
   /// Creates an immutable representation of a point coordinate in the [GoogleMap]'s view.
   const ScreenCoordinate({
-    @required this.x,
-    @required this.y,
+    required this.x,
+    required this.y,
   });
 
   /// Represents the number of pixels from the left of the [GoogleMap].
@@ -26,7 +26,7 @@
   final int y;
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() {
+  Object toJson() {
     return <String, int>{
       "x": x,
       "y": y,
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart
index a992b41..bfc3228 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart
@@ -21,13 +21,13 @@
   ///
   /// The image data format must be natively supported for decoding by the platform.
   /// e.g on Android it can only be one of the [supported image formats for decoding](https://developer.android.com/guide/topics/media/media-formats#image-formats).
-  final Uint8List data;
+  final Uint8List? data;
 
   /// Converts this object to JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  Object toJson() {
+    final Map<String, Object> json = <String, Object>{};
 
-    void addIfPresent(String fieldName, dynamic value) {
+    void addIfPresent(String fieldName, Object? value) {
       if (value != null) {
         json[fieldName] = value;
       }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart
index 3978f23..e31bfb4 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart
@@ -6,30 +6,13 @@
 import 'package:flutter/foundation.dart';
 
 import 'types.dart';
-import 'package:meta/meta.dart' show immutable, required;
+import 'package:meta/meta.dart' show immutable;
 
 /// Uniquely identifies a [TileOverlay] among [GoogleMap] tile overlays.
 @immutable
-class TileOverlayId {
+class TileOverlayId extends MapsObjectId<TileOverlay> {
   /// Creates an immutable identifier for a [TileOverlay].
-  TileOverlayId(this.value) : assert(value != null);
-
-  /// The value of the [TileOverlayId].
-  final String value;
-
-  @override
-  bool operator ==(Object other) {
-    if (other.runtimeType != runtimeType) {
-      return false;
-    }
-    return other is TileOverlayId && other.value == value;
-  }
-
-  @override
-  int get hashCode => value.hashCode;
-
-  @override
-  String toString() => '${objectRuntimeType(this, 'TileOverlayId')}($value)';
+  TileOverlayId(String value) : super(value);
 }
 
 /// A set of images which are displayed on top of the base map tiles.
@@ -61,14 +44,14 @@
 /// At zoom level N, the x values of the tile coordinates range from 0 to 2N - 1 and increase from
 /// west to east and the y values range from 0 to 2N - 1 and increase from north to south.
 ///
-class TileOverlay {
+class TileOverlay implements MapsObject {
   /// Creates an immutable representation of a [TileOverlay] to draw on [GoogleMap].
   const TileOverlay({
-    @required this.tileOverlayId,
+    required this.tileOverlayId,
     this.fadeIn = true,
     this.tileProvider,
     this.transparency = 0.0,
-    this.zIndex,
+    this.zIndex = 0,
     this.visible = true,
     this.tileSize = 256,
   }) : assert(transparency >= 0.0 && transparency <= 1.0);
@@ -76,11 +59,14 @@
   /// Uniquely identifies a [TileOverlay].
   final TileOverlayId tileOverlayId;
 
+  @override
+  TileOverlayId get mapsId => tileOverlayId;
+
   /// Whether the tiles should fade in. The default is true.
   final bool fadeIn;
 
   /// The tile provider to use for this tile overlay.
-  final TileProvider tileProvider;
+  final TileProvider? tileProvider;
 
   /// The transparency of the tile overlay. The default transparency is 0 (opaque).
   final double transparency;
@@ -104,12 +90,11 @@
   /// Creates a new [TileOverlay] object whose values are the same as this instance,
   /// unless overwritten by the specified parameters.
   TileOverlay copyWith({
-    TileOverlayId tileOverlayId,
-    bool fadeInParam,
-    double transparencyParam,
-    int zIndexParam,
-    bool visibleParam,
-    int tileSizeParam,
+    bool? fadeInParam,
+    double? transparencyParam,
+    int? zIndexParam,
+    bool? visibleParam,
+    int? tileSizeParam,
   }) {
     return TileOverlay(
       tileOverlayId: tileOverlayId,
@@ -121,11 +106,13 @@
     );
   }
 
-  /// Converts this object to JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  TileOverlay clone() => copyWith();
 
-    void addIfPresent(String fieldName, dynamic value) {
+  /// Converts this object to JSON.
+  Object toJson() {
+    final Map<String, Object> json = <String, Object>{};
+
+    void addIfPresent(String fieldName, Object? value) {
       if (value != null) {
         json[fieldName] = value;
       }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart
index 2910fc3..1436880 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart
@@ -2,121 +2,21 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter/foundation.dart' show objectRuntimeType, setEquals;
-import 'dart:ui' show hashValues, hashList;
-
-import 'utils/tile_overlay.dart';
 import 'types.dart';
 
 /// Update specification for a set of [TileOverlay]s.
-class TileOverlayUpdates {
+class TileOverlayUpdates extends MapsObjectUpdates<TileOverlay> {
   /// Computes [TileOverlayUpdates] given previous and current [TileOverlay]s.
-  TileOverlayUpdates.from(Set<TileOverlay> previous, Set<TileOverlay> current) {
-    if (previous == null) {
-      previous = Set<TileOverlay>.identity();
-    }
-
-    if (current == null) {
-      current = Set<TileOverlay>.identity();
-    }
-
-    final Map<TileOverlayId, TileOverlay> previousTileOverlays =
-        keyTileOverlayId(previous);
-    final Map<TileOverlayId, TileOverlay> currentTileOverlays =
-        keyTileOverlayId(current);
-
-    final Set<TileOverlayId> prevTileOverlayIds =
-        previousTileOverlays.keys.toSet();
-    final Set<TileOverlayId> currentTileOverlayIds =
-        currentTileOverlays.keys.toSet();
-
-    TileOverlay idToCurrentTileOverlay(TileOverlayId id) {
-      return currentTileOverlays[id];
-    }
-
-    _tileOverlayIdsToRemove =
-        prevTileOverlayIds.difference(currentTileOverlayIds);
-
-    _tileOverlaysToAdd = currentTileOverlayIds
-        .difference(prevTileOverlayIds)
-        .map(idToCurrentTileOverlay)
-        .toSet();
-
-    // Returns `true` if [current] is not equals to previous one with the
-    // same id.
-    bool hasChanged(TileOverlay current) {
-      final TileOverlay previous = previousTileOverlays[current.tileOverlayId];
-      return current != previous;
-    }
-
-    _tileOverlaysToChange = currentTileOverlayIds
-        .intersection(prevTileOverlayIds)
-        .map(idToCurrentTileOverlay)
-        .where(hasChanged)
-        .toSet();
-  }
+  TileOverlayUpdates.from(Set<TileOverlay> previous, Set<TileOverlay> current)
+      : super.from(previous, current, objectName: 'tileOverlay');
 
   /// Set of TileOverlays to be added in this update.
-  Set<TileOverlay> get tileOverlaysToAdd {
-    return _tileOverlaysToAdd;
-  }
-
-  Set<TileOverlay> _tileOverlaysToAdd;
+  Set<TileOverlay> get tileOverlaysToAdd => objectsToAdd;
 
   /// Set of TileOverlayIds to be removed in this update.
-  Set<TileOverlayId> get tileOverlayIdsToRemove {
-    return _tileOverlayIdsToRemove;
-  }
-
-  Set<TileOverlayId> _tileOverlayIdsToRemove;
+  Set<TileOverlayId> get tileOverlayIdsToRemove =>
+      objectIdsToRemove.cast<TileOverlayId>();
 
   /// Set of TileOverlays to be changed in this update.
-  Set<TileOverlay> get tileOverlaysToChange {
-    return _tileOverlaysToChange;
-  }
-
-  Set<TileOverlay> _tileOverlaysToChange;
-
-  /// Converts this object to JSON.
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> updateMap = <String, dynamic>{};
-
-    void addIfNonNull(String fieldName, dynamic value) {
-      if (value != null) {
-        updateMap[fieldName] = value;
-      }
-    }
-
-    addIfNonNull(
-        'tileOverlaysToAdd', serializeTileOverlaySet(_tileOverlaysToAdd));
-    addIfNonNull(
-        'tileOverlaysToChange', serializeTileOverlaySet(_tileOverlaysToChange));
-    addIfNonNull(
-        'tileOverlayIdsToRemove',
-        _tileOverlayIdsToRemove
-            .map<dynamic>((TileOverlayId m) => m.value)
-            .toList());
-
-    return updateMap;
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (other.runtimeType != runtimeType) {
-      return false;
-    }
-    return other is TileOverlayUpdates &&
-        setEquals(_tileOverlaysToAdd, other._tileOverlaysToAdd) &&
-        setEquals(_tileOverlayIdsToRemove, other._tileOverlayIdsToRemove) &&
-        setEquals(_tileOverlaysToChange, other._tileOverlaysToChange);
-  }
-
-  @override
-  int get hashCode => hashValues(hashList(_tileOverlaysToAdd),
-      hashList(_tileOverlayIdsToRemove), hashList(_tileOverlaysToChange));
-
-  @override
-  String toString() {
-    return '${objectRuntimeType(this, 'TileOverlayUpdates')}($_tileOverlaysToAdd, $_tileOverlayIdsToRemove, $_tileOverlaysToChange)';
-  }
+  Set<TileOverlay> get tileOverlaysToChange => objectsToChange;
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart
index c3c4f80..abaf38b 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart
@@ -12,5 +12,5 @@
   /// Returns the tile to be used for this tile coordinate.
   ///
   /// See [TileOverlay] for the specification of tile coordinates.
-  Future<Tile> getTile(int x, int y, int zoom);
+  Future<Tile> getTile(int x, int y, int? zoom);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart
index 3e2002f..9b7da87 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart
@@ -11,6 +11,8 @@
 export 'circle.dart';
 export 'joint_type.dart';
 export 'location.dart';
+export 'maps_object_updates.dart';
+export 'maps_object.dart';
 export 'marker_updates.dart';
 export 'marker.dart';
 export 'pattern_item.dart';
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart
index 8d84171..1c030e3 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart
@@ -39,19 +39,19 @@
   /// The geographical bounding box for the map camera target.
   ///
   /// A null value means the camera target is unbounded.
-  final LatLngBounds bounds;
+  final LatLngBounds? bounds;
 
   /// Unbounded camera target.
   static const CameraTargetBounds unbounded = CameraTargetBounds(null);
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() => <dynamic>[bounds?.toJson()];
+  Object toJson() => <Object?>[bounds?.toJson()];
 
   @override
-  bool operator ==(dynamic other) {
+  bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (runtimeType != other.runtimeType) return false;
-    final CameraTargetBounds typedOther = other;
+    final CameraTargetBounds typedOther = other as CameraTargetBounds;
     return bounds == typedOther.bounds;
   }
 
@@ -76,23 +76,23 @@
       : assert(minZoom == null || maxZoom == null || minZoom <= maxZoom);
 
   /// The preferred minimum zoom level or null, if unbounded from below.
-  final double minZoom;
+  final double? minZoom;
 
   /// The preferred maximum zoom level or null, if unbounded from above.
-  final double maxZoom;
+  final double? maxZoom;
 
   /// Unbounded zooming.
   static const MinMaxZoomPreference unbounded =
       MinMaxZoomPreference(null, null);
 
   /// Converts this object to something serializable in JSON.
-  dynamic toJson() => <dynamic>[minZoom, maxZoom];
+  Object toJson() => <Object?>[minZoom, maxZoom];
 
   @override
-  bool operator ==(dynamic other) {
+  bool operator ==(Object other) {
     if (identical(this, other)) return true;
     if (runtimeType != other.runtimeType) return false;
-    final MinMaxZoomPreference typedOther = other;
+    final MinMaxZoomPreference typedOther = other as MinMaxZoomPreference;
     return minZoom == typedOther.minZoom && maxZoom == typedOther.maxZoom;
   }
 
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart
index 5c3af96..18bd31e 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart
@@ -3,20 +3,14 @@
 // found in the LICENSE file.
 
 import '../types.dart';
+import 'maps_object.dart';
 
 /// Converts an [Iterable] of Circles in a Map of CircleId -> Circle.
 Map<CircleId, Circle> keyByCircleId(Iterable<Circle> circles) {
-  if (circles == null) {
-    return <CircleId, Circle>{};
-  }
-  return Map<CircleId, Circle>.fromEntries(circles.map((Circle circle) =>
-      MapEntry<CircleId, Circle>(circle.circleId, circle.clone())));
+  return keyByMapsObjectId<Circle>(circles).cast<CircleId, Circle>();
 }
 
 /// Converts a Set of Circles into something serializable in JSON.
-List<Map<String, dynamic>> serializeCircleSet(Set<Circle> circles) {
-  if (circles == null) {
-    return null;
-  }
-  return circles.map<Map<String, dynamic>>((Circle p) => p.toJson()).toList();
+Object serializeCircleSet(Set<Circle> circles) {
+  return serializeMapsObjectSet(circles);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart
new file mode 100644
index 0000000..fa5a7e7
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart
@@ -0,0 +1,18 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../maps_object.dart';
+
+/// Converts an [Iterable] of [MapsObject]s in a Map of [MapObjectId] -> [MapObject].
+Map<MapsObjectId<T>, T> keyByMapsObjectId<T extends MapsObject>(
+    Iterable<T> objects) {
+  return Map<MapsObjectId<T>, T>.fromEntries(objects.map((T object) =>
+      MapEntry<MapsObjectId<T>, T>(
+          object.mapsId as MapsObjectId<T>, object.clone())));
+}
+
+/// Converts a Set of [MapsObject]s into something serializable in JSON.
+Object serializeMapsObjectSet(Set<MapsObject> mapsObjects) {
+  return mapsObjects.map<Object>((MapsObject p) => p.toJson()).toList();
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart
index 7a2c76d..057bebd 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart
@@ -3,20 +3,14 @@
 // found in the LICENSE file.
 
 import '../types.dart';
+import 'maps_object.dart';
 
 /// Converts an [Iterable] of Markers in a Map of MarkerId -> Marker.
 Map<MarkerId, Marker> keyByMarkerId(Iterable<Marker> markers) {
-  if (markers == null) {
-    return <MarkerId, Marker>{};
-  }
-  return Map<MarkerId, Marker>.fromEntries(markers.map((Marker marker) =>
-      MapEntry<MarkerId, Marker>(marker.markerId, marker.clone())));
+  return keyByMapsObjectId<Marker>(markers).cast<MarkerId, Marker>();
 }
 
 /// Converts a Set of Markers into something serializable in JSON.
-List<Map<String, dynamic>> serializeMarkerSet(Set<Marker> markers) {
-  if (markers == null) {
-    return null;
-  }
-  return markers.map<Map<String, dynamic>>((Marker m) => m.toJson()).toList();
+Object serializeMarkerSet(Set<Marker> markers) {
+  return serializeMapsObjectSet(markers);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart
index 9434dda..050ecaf 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart
@@ -3,20 +3,14 @@
 // found in the LICENSE file.
 
 import '../types.dart';
+import 'maps_object.dart';
 
 /// Converts an [Iterable] of Polygons in a Map of PolygonId -> Polygon.
 Map<PolygonId, Polygon> keyByPolygonId(Iterable<Polygon> polygons) {
-  if (polygons == null) {
-    return <PolygonId, Polygon>{};
-  }
-  return Map<PolygonId, Polygon>.fromEntries(polygons.map((Polygon polygon) =>
-      MapEntry<PolygonId, Polygon>(polygon.polygonId, polygon.clone())));
+  return keyByMapsObjectId<Polygon>(polygons).cast<PolygonId, Polygon>();
 }
 
 /// Converts a Set of Polygons into something serializable in JSON.
-List<Map<String, dynamic>> serializePolygonSet(Set<Polygon> polygons) {
-  if (polygons == null) {
-    return null;
-  }
-  return polygons.map<Map<String, dynamic>>((Polygon p) => p.toJson()).toList();
+Object serializePolygonSet(Set<Polygon> polygons) {
+  return serializeMapsObjectSet(polygons);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart
index 9cef631..8f4098f 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart
@@ -3,23 +3,14 @@
 // found in the LICENSE file.
 
 import '../types.dart';
+import 'maps_object.dart';
 
 /// Converts an [Iterable] of Polylines in a Map of PolylineId -> Polyline.
 Map<PolylineId, Polyline> keyByPolylineId(Iterable<Polyline> polylines) {
-  if (polylines == null) {
-    return <PolylineId, Polyline>{};
-  }
-  return Map<PolylineId, Polyline>.fromEntries(polylines.map(
-      (Polyline polyline) => MapEntry<PolylineId, Polyline>(
-          polyline.polylineId, polyline.clone())));
+  return keyByMapsObjectId<Polyline>(polylines).cast<PolylineId, Polyline>();
 }
 
 /// Converts a Set of Polylines into something serializable in JSON.
-List<Map<String, dynamic>> serializePolylineSet(Set<Polyline> polylines) {
-  if (polylines == null) {
-    return null;
-  }
-  return polylines
-      .map<Map<String, dynamic>>((Polyline p) => p.toJson())
-      .toList();
+Object serializePolylineSet(Set<Polyline> polylines) {
+  return serializeMapsObjectSet(polylines);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart
index 0736c83..336f814 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart
@@ -1,23 +1,18 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 import '../types.dart';
+import 'maps_object.dart';
 
 /// Converts an [Iterable] of TileOverlay in a Map of TileOverlayId -> TileOverlay.
 Map<TileOverlayId, TileOverlay> keyTileOverlayId(
     Iterable<TileOverlay> tileOverlays) {
-  if (tileOverlays == null) {
-    return <TileOverlayId, TileOverlay>{};
-  }
-  return Map<TileOverlayId, TileOverlay>.fromEntries(tileOverlays.map(
-      (TileOverlay tileOverlay) => MapEntry<TileOverlayId, TileOverlay>(
-          tileOverlay.tileOverlayId, tileOverlay)));
+  return keyByMapsObjectId<TileOverlay>(tileOverlays)
+      .cast<TileOverlayId, TileOverlay>();
 }
 
 /// Converts a Set of TileOverlays into something serializable in JSON.
-List<Map<String, dynamic>> serializeTileOverlaySet(
-    Set<TileOverlay> tileOverlays) {
-  if (tileOverlays == null) {
-    return null;
-  }
-  return tileOverlays
-      .map<Map<String, dynamic>>((TileOverlay p) => p.toJson())
-      .toList();
+Object serializeTileOverlaySet(Set<TileOverlay> tileOverlays) {
+  return serializeMapsObjectSet(tileOverlays);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
index 487a54b..8b31fee 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
@@ -3,22 +3,22 @@
 homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_platform_interface
 # NOTE: We strongly prefer non-breaking changes, even at the expense of a
 # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 1.2.0
+version: 2.0.0-nullsafety
 
 dependencies:
   flutter:
     sdk: flutter
   meta: ^1.0.5
-  plugin_platform_interface: ^1.0.1
-  stream_transform: ^1.2.0
+  plugin_platform_interface: ^1.1.0-nullsafety.2
+  stream_transform: ^2.0.0-nullsafety.0
   collection: ^1.14.13
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
-  mockito: ^4.1.1
+  mockito: ^5.0.0-nullsafety.0
   pedantic: ^1.8.0
 
 environment:
-  sdk: ">=2.3.0 <3.0.0"
+  sdk: '>=2.12.0-0 <3.0.0'
   flutter: ">=1.9.1+hotfix.4"
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart
new file mode 100644
index 0000000..65692bd
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart
@@ -0,0 +1,45 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+import 'package:google_maps_flutter_platform_interface/src/types/utils/maps_object.dart';
+
+import 'test_maps_object.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  test('keyByMapsObjectId', () async {
+    final MapsObjectId<TestMapsObject> id1 = MapsObjectId<TestMapsObject>('1');
+    final MapsObjectId<TestMapsObject> id2 = MapsObjectId<TestMapsObject>('2');
+    final MapsObjectId<TestMapsObject> id3 = MapsObjectId<TestMapsObject>('3');
+    final TestMapsObject object1 = TestMapsObject(id1);
+    final TestMapsObject object2 = TestMapsObject(id2, data: 2);
+    final TestMapsObject object3 = TestMapsObject(id3);
+    expect(
+        keyByMapsObjectId(<TestMapsObject>{object1, object2, object3}),
+        <MapsObjectId<TestMapsObject>, TestMapsObject>{
+          id1: object1,
+          id2: object2,
+          id3: object3,
+        });
+  });
+
+  test('serializeMapsObjectSet', () async {
+    final MapsObjectId<TestMapsObject> id1 = MapsObjectId<TestMapsObject>('1');
+    final MapsObjectId<TestMapsObject> id2 = MapsObjectId<TestMapsObject>('2');
+    final MapsObjectId<TestMapsObject> id3 = MapsObjectId<TestMapsObject>('3');
+    final TestMapsObject object1 = TestMapsObject(id1);
+    final TestMapsObject object2 = TestMapsObject(id2, data: 2);
+    final TestMapsObject object3 = TestMapsObject(id3);
+    expect(
+        serializeMapsObjectSet(<TestMapsObject>{object1, object2, object3}),
+        <Map<String, Object>>[
+          {'id': '1'},
+          {'id': '2'},
+          {'id': '3'}
+        ]);
+  });
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart
new file mode 100644
index 0000000..68f4c58
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart
@@ -0,0 +1,160 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui' show hashValues, hashList;
+
+import 'package:flutter/rendering.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter_platform_interface/src/types/utils/maps_object.dart';
+import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart';
+import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart';
+
+import 'test_maps_object.dart';
+
+class TestMapsObjectUpdate extends MapsObjectUpdates<TestMapsObject> {
+  TestMapsObjectUpdate.from(
+      Set<TestMapsObject> previous, Set<TestMapsObject> current)
+      : super.from(previous, current, objectName: 'testObject');
+}
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('tile overlay updates tests', () {
+    test('Correctly set toRemove, toAdd and toChange', () async {
+      final TestMapsObject to1 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id1'));
+      final TestMapsObject to2 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id2'));
+      final TestMapsObject to3 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'));
+      final TestMapsObject to3Changed =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'), data: 2);
+      final TestMapsObject to4 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id4'));
+      final Set<TestMapsObject> previous =
+          Set.from(<TestMapsObject>[to1, to2, to3]);
+      final Set<TestMapsObject> current =
+          Set.from(<TestMapsObject>[to2, to3Changed, to4]);
+      final TestMapsObjectUpdate updates =
+          TestMapsObjectUpdate.from(previous, current);
+
+      final Set<MapsObjectId<TestMapsObject>> toRemove = Set.from(
+          <MapsObjectId<TestMapsObject>>[MapsObjectId<TestMapsObject>('id1')]);
+      expect(updates.objectIdsToRemove, toRemove);
+
+      final Set<TestMapsObject> toAdd = Set.from(<TestMapsObject>[to4]);
+      expect(updates.objectsToAdd, toAdd);
+
+      final Set<TestMapsObject> toChange =
+          Set.from(<TestMapsObject>[to3Changed]);
+      expect(updates.objectsToChange, toChange);
+    });
+
+    test('toJson', () async {
+      final TestMapsObject to1 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id1'));
+      final TestMapsObject to2 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id2'));
+      final TestMapsObject to3 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'));
+      final TestMapsObject to3Changed =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'), data: 2);
+      final TestMapsObject to4 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id4'));
+      final Set<TestMapsObject> previous =
+          Set.from(<TestMapsObject>[to1, to2, to3]);
+      final Set<TestMapsObject> current =
+          Set.from(<TestMapsObject>[to2, to3Changed, to4]);
+      final TestMapsObjectUpdate updates =
+          TestMapsObjectUpdate.from(previous, current);
+
+      final Object json = updates.toJson();
+      expect(json, <String, Object>{
+        'testObjectsToAdd': serializeMapsObjectSet(updates.objectsToAdd),
+        'testObjectsToChange': serializeMapsObjectSet(updates.objectsToChange),
+        'testObjectIdsToRemove': updates.objectIdsToRemove
+            .map<String>((MapsObjectId<TestMapsObject> m) => m.value)
+            .toList()
+      });
+    });
+
+    test('equality', () async {
+      final TestMapsObject to1 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id1'));
+      final TestMapsObject to2 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id2'));
+      final TestMapsObject to3 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'));
+      final TestMapsObject to3Changed =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'), data: 2);
+      final TestMapsObject to4 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id4'));
+      final Set<TestMapsObject> previous =
+          Set.from(<TestMapsObject>[to1, to2, to3]);
+      final Set<TestMapsObject> current1 =
+          Set.from(<TestMapsObject>[to2, to3Changed, to4]);
+      final Set<TestMapsObject> current2 =
+          Set.from(<TestMapsObject>[to2, to3Changed, to4]);
+      final Set<TestMapsObject> current3 = Set.from(<TestMapsObject>[to2, to4]);
+      final TestMapsObjectUpdate updates1 =
+          TestMapsObjectUpdate.from(previous, current1);
+      final TestMapsObjectUpdate updates2 =
+          TestMapsObjectUpdate.from(previous, current2);
+      final TestMapsObjectUpdate updates3 =
+          TestMapsObjectUpdate.from(previous, current3);
+      expect(updates1, updates2);
+      expect(updates1, isNot(updates3));
+    });
+
+    test('hashCode', () async {
+      final TestMapsObject to1 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id1'));
+      final TestMapsObject to2 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id2'));
+      final TestMapsObject to3 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'));
+      final TestMapsObject to3Changed =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'), data: 2);
+      final TestMapsObject to4 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id4'));
+      final Set<TestMapsObject> previous =
+          Set.from(<TestMapsObject>[to1, to2, to3]);
+      final Set<TestMapsObject> current =
+          Set.from(<TestMapsObject>[to2, to3Changed, to4]);
+      final TestMapsObjectUpdate updates =
+          TestMapsObjectUpdate.from(previous, current);
+      expect(
+          updates.hashCode,
+          hashValues(
+              hashList(updates.objectsToAdd),
+              hashList(updates.objectIdsToRemove),
+              hashList(updates.objectsToChange)));
+    });
+
+    test('toString', () async {
+      final TestMapsObject to1 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id1'));
+      final TestMapsObject to2 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id2'));
+      final TestMapsObject to3 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'));
+      final TestMapsObject to3Changed =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id3'), data: 2);
+      final TestMapsObject to4 =
+          TestMapsObject(MapsObjectId<TestMapsObject>('id4'));
+      final Set<TestMapsObject> previous =
+          Set.from(<TestMapsObject>[to1, to2, to3]);
+      final Set<TestMapsObject> current =
+          Set.from(<TestMapsObject>[to2, to3Changed, to4]);
+      final TestMapsObjectUpdate updates =
+          TestMapsObjectUpdate.from(previous, current);
+      expect(
+          updates.toString(),
+          'TestMapsObjectUpdate(add: ${updates.objectsToAdd}, '
+          'remove: ${updates.objectIdsToRemove}, '
+          'change: ${updates.objectsToChange})');
+    });
+  });
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart
new file mode 100644
index 0000000..e15c73f
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart
@@ -0,0 +1,46 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui' show hashValues;
+import 'package:flutter/rendering.dart';
+import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart';
+import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart';
+
+/// A trivial TestMapsObject implementation for testing updates with.
+class TestMapsObject implements MapsObject {
+  TestMapsObject(this.mapsId, {this.data = 1});
+
+  final MapsObjectId<TestMapsObject> mapsId;
+
+  final int data;
+
+  @override
+  TestMapsObject clone() {
+    return TestMapsObject(mapsId, data: data);
+  }
+
+  @override
+  Object toJson() {
+    return <String, Object>{'id': mapsId.value};
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is TestMapsObject &&
+        mapsId == other.mapsId &&
+        data == other.data;
+  }
+
+  @override
+  int get hashCode => hashValues(mapsId, data);
+}
+
+class TestMapsObjectUpdate extends MapsObjectUpdates<TestMapsObject> {
+  TestMapsObjectUpdate.from(
+      Set<TestMapsObject> previous, Set<TestMapsObject> current)
+      : super.from(previous, current, objectName: 'testObject');
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart
index bb0621d..87380fd 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart
@@ -34,13 +34,15 @@
           zIndex: 1,
           visible: false,
           tileSize: 128);
-      final Map<String, dynamic> json = tileOverlay.toJson();
-      expect(json['tileOverlayId'], 'id');
-      expect(json['fadeIn'], false);
-      expect(json['transparency'], moreOrLessEquals(0.1));
-      expect(json['zIndex'], 1);
-      expect(json['visible'], false);
-      expect(json['tileSize'], 128);
+      final Object json = tileOverlay.toJson();
+      expect(json, <String, Object>{
+        'tileOverlayId': 'id',
+        'fadeIn': false,
+        'transparency': moreOrLessEquals(0.1),
+        'zIndex': 1,
+        'visible': false,
+        'tileSize': 128,
+      });
     });
 
     test('invalid transparency throws', () async {
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart
index 980a203..f622ca5 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart
@@ -49,13 +49,13 @@
       final TileOverlayUpdates updates =
           TileOverlayUpdates.from(previous, current);
 
-      final Map<String, dynamic> json = updates.toJson();
-      expect(json, <String, dynamic>{
+      final Object json = updates.toJson();
+      expect(json, <String, Object>{
         'tileOverlaysToAdd': serializeTileOverlaySet(updates.tileOverlaysToAdd),
         'tileOverlaysToChange':
             serializeTileOverlaySet(updates.tileOverlaysToChange),
         'tileOverlayIdsToRemove': updates.tileOverlayIdsToRemove
-            .map<dynamic>((TileOverlayId m) => m.value)
+            .map<String>((TileOverlayId m) => m.value)
             .toList()
       });
     });
@@ -117,9 +117,9 @@
           TileOverlayUpdates.from(previous, current);
       expect(
           updates.toString(),
-          'TileOverlayUpdates(${updates.tileOverlaysToAdd}, '
-          '${updates.tileOverlayIdsToRemove}, '
-          '${updates.tileOverlaysToChange})');
+          'TileOverlayUpdates(add: ${updates.tileOverlaysToAdd}, '
+          'remove: ${updates.tileOverlayIdsToRemove}, '
+          'change: ${updates.tileOverlaysToChange})');
     });
   });
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart
index 0be9a7c..3e0fe99 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart
@@ -13,16 +13,21 @@
     test('toJson returns correct format', () async {
       final Uint8List data = Uint8List.fromList([0, 1]);
       final Tile tile = Tile(100, 200, data);
-      final Map<String, dynamic> json = tile.toJson();
-      expect(json['width'], 100);
-      expect(json['height'], 200);
-      expect(json['data'], data);
+      final Object json = tile.toJson();
+      expect(json, <String, Object>{
+        'width': 100,
+        'height': 200,
+        'data': data,
+      });
     });
 
-    test('toJson returns empty if nothing presents', () async {
-      final Tile tile = Tile(null, null, null);
-      final Map<String, dynamic> json = tile.toJson();
-      expect(json.isEmpty, true);
+    test('toJson handles null data', () async {
+      final Tile tile = Tile(0, 0, null);
+      final Object json = tile.toJson();
+      expect(json, <String, Object>{
+        'width': 0,
+        'height': 0,
+      });
     });
   });
 }
diff --git a/script/nnbd_plugins.sh b/script/nnbd_plugins.sh
index 5a54a7f..3e82ac2 100644
--- a/script/nnbd_plugins.sh
+++ b/script/nnbd_plugins.sh
@@ -15,6 +15,7 @@
   "file_selector"
   "flutter_plugin_android_lifecycle"
   "flutter_webview"
+  "google_maps_flutter"
   "google_sign_in"
   "image_picker"
   "ios_platform_images"
@@ -38,7 +39,7 @@
 
 readonly NON_NNBD_PLUGINS_LIST=(
   "camera"
-  # "google_maps_flutter"
+  "google_maps_flutter" # half migrated
   # "image_picker"
   # "in_app_purchase"
   # "quick_actions"