[google_maps_flutter] Clone cached elements in GoogleMap (#2076)

Create a clone of cached elements in GoogleMap (Polyline, Polygon, etc.)
to detect modifications if these objects are mutated instead of
modified by copy.
diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md
index 56fbf0e..45ecfbe 100644
--- a/packages/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.5.21+7
+
+* Create a clone of cached elements in GoogleMap (Polyline, Polygon, etc.) to detect modifications
+  if these objects are mutated instead of modified by copy.
+
 ## 0.5.21+6
 
 * Override a default method to work around flutter/flutter#40126.
diff --git a/packages/google_maps_flutter/lib/src/circle.dart b/packages/google_maps_flutter/lib/src/circle.dart
index eefb8c0..1ab966c 100644
--- a/packages/google_maps_flutter/lib/src/circle.dart
+++ b/packages/google_maps_flutter/lib/src/circle.dart
@@ -114,6 +114,9 @@
     );
   }
 
+  /// Creates a new [Circle] object whose values are the same as this instance.
+  Circle clone() => copyWith();
+
   dynamic _toJson() {
     final Map<String, dynamic> json = <String, dynamic>{};
 
@@ -161,8 +164,8 @@
   if (circles == null) {
     return <CircleId, Circle>{};
   }
-  return Map<CircleId, Circle>.fromEntries(circles.map(
-      (Circle circle) => MapEntry<CircleId, Circle>(circle.circleId, circle)));
+  return Map<CircleId, Circle>.fromEntries(circles.map((Circle circle) =>
+      MapEntry<CircleId, Circle>(circle.circleId, circle.clone())));
 }
 
 List<Map<String, dynamic>> _serializeCircleSet(Set<Circle> circles) {
diff --git a/packages/google_maps_flutter/lib/src/marker.dart b/packages/google_maps_flutter/lib/src/marker.dart
index 4a087f7..5d4d4f3 100644
--- a/packages/google_maps_flutter/lib/src/marker.dart
+++ b/packages/google_maps_flutter/lib/src/marker.dart
@@ -254,6 +254,9 @@
     );
   }
 
+  /// Creates a new [Marker] object whose values are the same as this instance.
+  Marker clone() => copyWith();
+
   Map<String, dynamic> _toJson() {
     final Map<String, dynamic> json = <String, dynamic>{};
 
@@ -314,8 +317,8 @@
   if (markers == null) {
     return <MarkerId, Marker>{};
   }
-  return Map<MarkerId, Marker>.fromEntries(markers.map(
-      (Marker marker) => MapEntry<MarkerId, Marker>(marker.markerId, marker)));
+  return Map<MarkerId, Marker>.fromEntries(markers.map((Marker marker) =>
+      MapEntry<MarkerId, Marker>(marker.markerId, marker.clone())));
 }
 
 List<Map<String, dynamic>> _serializeMarkerSet(Set<Marker> markers) {
diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart
index 2230ae8..4aed3bd 100644
--- a/packages/google_maps_flutter/lib/src/polygon.dart
+++ b/packages/google_maps_flutter/lib/src/polygon.dart
@@ -120,6 +120,11 @@
     );
   }
 
+  /// Creates a new [Polygon] object whose values are the same as this instance.
+  Polygon clone() {
+    return copyWith(pointsParam: List<LatLng>.of(points));
+  }
+
   dynamic _toJson() {
     final Map<String, dynamic> json = <String, dynamic>{};
 
@@ -179,7 +184,7 @@
     return <PolygonId, Polygon>{};
   }
   return Map<PolygonId, Polygon>.fromEntries(polygons.map((Polygon polygon) =>
-      MapEntry<PolygonId, Polygon>(polygon.polygonId, polygon)));
+      MapEntry<PolygonId, Polygon>(polygon.polygonId, polygon.clone())));
 }
 
 List<Map<String, dynamic>> _serializePolygonSet(Set<Polygon> polygons) {
diff --git a/packages/google_maps_flutter/lib/src/polyline.dart b/packages/google_maps_flutter/lib/src/polyline.dart
index 7454711..1eac145 100644
--- a/packages/google_maps_flutter/lib/src/polyline.dart
+++ b/packages/google_maps_flutter/lib/src/polyline.dart
@@ -156,6 +156,15 @@
     );
   }
 
+  /// Creates a new [Polyline] object whose values are the same as this
+  /// instance.
+  Polyline clone() {
+    return copyWith(
+      patternsParam: List<PatternItem>.of(patterns),
+      pointsParam: List<LatLng>.of(points),
+    );
+  }
+
   dynamic _toJson() {
     final Map<String, dynamic> json = <String, dynamic>{};
 
@@ -234,8 +243,8 @@
     return <PolylineId, Polyline>{};
   }
   return Map<PolylineId, Polyline>.fromEntries(polylines.map(
-      (Polyline polyline) =>
-          MapEntry<PolylineId, Polyline>(polyline.polylineId, polyline)));
+      (Polyline polyline) => MapEntry<PolylineId, Polyline>(
+          polyline.polylineId, polyline.clone())));
 }
 
 List<Map<String, dynamic>> _serializePolylineSet(Set<Polyline> polylines) {
diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml
index 71d870f..5e8e92e 100644
--- a/packages/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
 author: Flutter Team <flutter-dev@googlegroups.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter
-version: 0.5.21+6
+version: 0.5.21+7
 
 dependencies:
   flutter:
diff --git a/packages/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/test/fake_maps_controllers.dart
index 0ae12c8..b92b841 100644
--- a/packages/google_maps_flutter/test/fake_maps_controllers.dart
+++ b/packages/google_maps_flutter/test/fake_maps_controllers.dart
@@ -195,17 +195,25 @@
       final String polygonId = polygonData['polygonId'];
       final bool visible = polygonData['visible'];
       final bool geodesic = polygonData['geodesic'];
+      final List<LatLng> points = _deserializePoints(polygonData['points']);
 
       result.add(Polygon(
         polygonId: PolygonId(polygonId),
         visible: visible,
         geodesic: geodesic,
+        points: points,
       ));
     }
 
     return result;
   }
 
+  List<LatLng> _deserializePoints(List<dynamic> points) {
+    return points.map<LatLng>((dynamic list) {
+      return LatLng(list[0], list[1]);
+    }).toList();
+  }
+
   void updatePolylines(Map<dynamic, dynamic> polylineUpdates) {
     if (polylineUpdates == null) {
       return;
@@ -245,11 +253,13 @@
       final String polylineId = polylineData['polylineId'];
       final bool visible = polylineData['visible'];
       final bool geodesic = polylineData['geodesic'];
+      final List<LatLng> points = _deserializePoints(polylineData['points']);
 
       result.add(Polyline(
         polylineId: PolylineId(polylineId),
         visible: visible,
         geodesic: geodesic,
+        points: points,
       ));
     }
 
diff --git a/packages/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/test/polygon_updates_test.dart
index c666cb6..a884254 100644
--- a/packages/google_maps_flutter/test/polygon_updates_test.dart
+++ b/packages/google_maps_flutter/test/polygon_updates_test.dart
@@ -130,6 +130,25 @@
     expect(update.geodesic, true);
   });
 
+  testWidgets("Mutate a polygon", (WidgetTester tester) async {
+    final Polygon p1 = Polygon(
+      polygonId: PolygonId("polygon_1"),
+      points: <LatLng>[const LatLng(0.0, 0.0)],
+    );
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+
+    p1.points.add(const LatLng(1.0, 1.0));
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.polygonsToChange.length, 1);
+    expect(platformGoogleMap.polygonsToChange.first, equals(p1));
+
+    expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+  });
+
   testWidgets("Multi Update", (WidgetTester tester) async {
     Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
     Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
diff --git a/packages/google_maps_flutter/test/polyline_updates_test.dart b/packages/google_maps_flutter/test/polyline_updates_test.dart
index a0f3918..ddc64bf 100644
--- a/packages/google_maps_flutter/test/polyline_updates_test.dart
+++ b/packages/google_maps_flutter/test/polyline_updates_test.dart
@@ -130,6 +130,25 @@
     expect(update.geodesic, true);
   });
 
+  testWidgets("Mutate a polyline", (WidgetTester tester) async {
+    final Polyline p1 = Polyline(
+      polylineId: PolylineId("polyline_1"),
+      points: <LatLng>[const LatLng(0.0, 0.0)],
+    );
+    await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1)));
+
+    p1.points.add(const LatLng(1.0, 1.0));
+    await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.polylinesToChange.length, 1);
+    expect(platformGoogleMap.polylinesToChange.first, equals(p1));
+
+    expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.polylinesToAdd.isEmpty, true);
+  });
+
   testWidgets("Multi Update", (WidgetTester tester) async {
     Polyline p1 = Polyline(polylineId: PolylineId("polyline_1"));
     Polyline p2 = Polyline(polylineId: PolylineId("polyline_2"));