Adds support for polygon overlays to the Google Maps plugin (#1551)

This is relatively trivial, requiring only some additional logic
to disambiguate click events between the various possible overlays.

Also adds a page to the example app demonstrating polygons,
which I tested on iOS and Android.
diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md
index 9ecdd88..d7e623d 100644
--- a/packages/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.5.15
+
+* Add support for Polygons.
+
 ## 0.5.14+1
 
 * Example app update(comment out usage of the ImageStreamListener API which has a breaking change
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
index 46ae935..b8775b9 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
@@ -170,6 +170,15 @@
     return data;
   }
 
+  static Object polygonIdToJson(String polygonId) {
+    if (polygonId == null) {
+      return null;
+    }
+    final Map<String, Object> data = new HashMap<>(1);
+    data.put("polygonId", polygonId);
+    return data;
+  }
+
   static Object polylineIdToJson(String polylineId) {
     if (polylineId == null) {
       return null;
@@ -364,6 +373,48 @@
     }
   }
 
+  static String interpretPolygonOptions(Object o, PolygonOptionsSink sink) {
+    final Map<?, ?> data = toMap(o);
+    final Object consumeTapEvents = data.get("consumeTapEvents");
+    if (consumeTapEvents != null) {
+      sink.setConsumeTapEvents(toBoolean(consumeTapEvents));
+    }
+    final Object geodesic = data.get("geodesic");
+    if (geodesic != null) {
+      sink.setGeodesic(toBoolean(geodesic));
+    }
+    final Object visible = data.get("visible");
+    if (visible != null) {
+      sink.setVisible(toBoolean(visible));
+    }
+    final Object fillColor = data.get("fillColor");
+    if (fillColor != null) {
+      sink.setFillColor(toInt(fillColor));
+    }
+    final Object strokeColor = data.get("strokeColor");
+    if (strokeColor != null) {
+      sink.setStrokeColor(toInt(strokeColor));
+    }
+    final Object strokeWidth = data.get("strokeWidth");
+    if (strokeWidth != null) {
+      sink.setStrokeWidth(toInt(strokeWidth));
+    }
+    final Object zIndex = data.get("zIndex");
+    if (zIndex != null) {
+      sink.setZIndex(toFloat(zIndex));
+    }
+    final Object points = data.get("points");
+    if (points != null) {
+      sink.setPoints(toPoints(points));
+    }
+    final String polygonId = (String) data.get("polygonId");
+    if (polygonId == null) {
+      throw new IllegalArgumentException("polygonId was null");
+    } else {
+      return polygonId;
+    }
+  }
+
   static String interpretPolylineOptions(Object o, PolylineOptionsSink sink) {
     final Map<?, ?> data = toMap(o);
     final Object consumeTapEvents = data.get("consumeTapEvents");
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
index bb3d750..49bad46 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
@@ -17,6 +17,7 @@
   private boolean myLocationEnabled = false;
   private boolean myLocationButtonEnabled = false;
   private Object initialMarkers;
+  private Object initialPolygons;
   private Object initialPolylines;
   private Object initialCircles;
 
@@ -29,6 +30,7 @@
     controller.setMyLocationButtonEnabled(myLocationButtonEnabled);
     controller.setTrackCameraPosition(trackCameraPosition);
     controller.setInitialMarkers(initialMarkers);
+    controller.setInitialPolygons(initialPolygons);
     controller.setInitialPolylines(initialPolylines);
     controller.setInitialCircles(initialCircles);
     return controller;
@@ -104,6 +106,11 @@
   }
 
   @Override
+  public void setInitialPolygons(Object initialPolygons) {
+    this.initialPolygons = initialPolygons;
+  }
+
+  @Override
   public void setInitialPolylines(Object initialPolylines) {
     this.initialPolylines = initialPolylines;
   }
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
index 338f33a..31f532a 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -30,6 +30,7 @@
 import com.google.android.gms.maps.model.LatLng;
 import com.google.android.gms.maps.model.LatLngBounds;
 import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.Polygon;
 import com.google.android.gms.maps.model.Polyline;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
@@ -50,6 +51,7 @@
         GoogleMap.OnCameraMoveStartedListener,
         GoogleMap.OnInfoWindowClickListener,
         GoogleMap.OnMarkerClickListener,
+        GoogleMap.OnPolygonClickListener,
         GoogleMap.OnPolylineClickListener,
         GoogleMap.OnCircleClickListener,
         GoogleMapOptionsSink,
@@ -75,9 +77,11 @@
   private final int registrarActivityHashCode;
   private final Context context;
   private final MarkersController markersController;
+  private final PolygonsController polygonsController;
   private final PolylinesController polylinesController;
   private final CirclesController circlesController;
   private List<Object> initialMarkers;
+  private List<Object> initialPolygons;
   private List<Object> initialPolylines;
   private List<Object> initialCircles;
 
@@ -98,6 +102,7 @@
     methodChannel.setMethodCallHandler(this);
     this.registrarActivityHashCode = registrar.activity().hashCode();
     this.markersController = new MarkersController(methodChannel);
+    this.polygonsController = new PolygonsController(methodChannel);
     this.polylinesController = new PolylinesController(methodChannel);
     this.circlesController = new CirclesController(methodChannel);
   }
@@ -169,15 +174,18 @@
     googleMap.setOnCameraMoveListener(this);
     googleMap.setOnCameraIdleListener(this);
     googleMap.setOnMarkerClickListener(this);
+    googleMap.setOnPolygonClickListener(this);
     googleMap.setOnPolylineClickListener(this);
     googleMap.setOnCircleClickListener(this);
     googleMap.setOnMapClickListener(this);
     googleMap.setOnMapLongClickListener(this);
     updateMyLocationSettings();
     markersController.setGoogleMap(googleMap);
+    polygonsController.setGoogleMap(googleMap);
     polylinesController.setGoogleMap(googleMap);
     circlesController.setGoogleMap(googleMap);
     updateInitialMarkers();
+    updateInitialPolygons();
     updateInitialPolylines();
     updateInitialCircles();
   }
@@ -238,6 +246,17 @@
           result.success(null);
           break;
         }
+      case "polygons#update":
+        {
+          Object polygonsToAdd = call.argument("polygonsToAdd");
+          polygonsController.addPolygons((List<Object>) polygonsToAdd);
+          Object polygonsToChange = call.argument("polygonsToChange");
+          polygonsController.changePolygons((List<Object>) polygonsToChange);
+          Object polygonIdsToRemove = call.argument("polygonIdsToRemove");
+          polygonsController.removePolygons((List<Object>) polygonIdsToRemove);
+          result.success(null);
+          break;
+        }
       case "polylines#update":
         {
           Object polylinesToAdd = call.argument("polylinesToAdd");
@@ -351,6 +370,11 @@
   }
 
   @Override
+  public void onPolygonClick(Polygon polygon) {
+    polygonsController.onPolygonTap(polygon.getId());
+  }
+
+  @Override
   public void onPolylineClick(Polyline polyline) {
     polylinesController.onPolylineTap(polyline.getId());
   }
@@ -515,6 +539,18 @@
   }
 
   @Override
+  public void setInitialPolygons(Object initialPolygons) {
+    this.initialPolygons = (List<Object>) initialPolygons;
+    if (googleMap != null) {
+      updateInitialPolygons();
+    }
+  }
+
+  private void updateInitialPolygons() {
+    polygonsController.addPolygons(initialPolygons);
+  }
+
+  @Override
   public void setInitialPolylines(Object initialPolylines) {
     this.initialPolylines = (List<Object>) initialPolylines;
     if (googleMap != null) {
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
index 1e1082a..bc19fa5 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
@@ -35,6 +35,9 @@
     if (params.containsKey("markersToAdd")) {
       builder.setInitialMarkers(params.get("markersToAdd"));
     }
+    if (params.containsKey("polygonsToAdd")) {
+      builder.setInitialPolygons(params.get("polygonsToAdd"));
+    }
     if (params.containsKey("polylinesToAdd")) {
       builder.setInitialPolylines(params.get("polylinesToAdd"));
     }
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
index 347b85d..5e11eb2 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
@@ -32,6 +32,8 @@
 
   void setInitialMarkers(Object initialMarkers);
 
+  void setInitialPolygons(Object initialPolygons);
+
   void setInitialPolylines(Object initialPolylines);
 
   void setInitialCircles(Object initialCircles);
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java
new file mode 100644
index 0000000..68c3586
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java
@@ -0,0 +1,63 @@
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.PolygonOptions;
+import java.util.List;
+
+class PolygonBuilder implements PolygonOptionsSink {
+  private final PolygonOptions polygonOptions;
+  private boolean consumeTapEvents;
+
+  PolygonBuilder() {
+    this.polygonOptions = new PolygonOptions();
+  }
+
+  PolygonOptions build() {
+    return polygonOptions;
+  }
+
+  boolean consumeTapEvents() {
+    return consumeTapEvents;
+  }
+
+  @Override
+  public void setFillColor(int color) {
+    polygonOptions.fillColor(color);
+  }
+
+  @Override
+  public void setStrokeColor(int color) {
+    polygonOptions.strokeColor(color);
+  }
+
+  @Override
+  public void setPoints(List<LatLng> points) {
+    polygonOptions.addAll(points);
+  }
+
+  @Override
+  public void setConsumeTapEvents(boolean consumeTapEvents) {
+    this.consumeTapEvents = consumeTapEvents;
+    polygonOptions.clickable(consumeTapEvents);
+  }
+
+  @Override
+  public void setGeodesic(boolean geodisc) {
+    polygonOptions.geodesic(geodisc);
+  }
+
+  @Override
+  public void setVisible(boolean visible) {
+    polygonOptions.visible(visible);
+  }
+
+  @Override
+  public void setStrokeWidth(float width) {
+    polygonOptions.strokeWidth(width);
+  }
+
+  @Override
+  public void setZIndex(float zIndex) {
+    polygonOptions.zIndex(zIndex);
+  }
+}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java
new file mode 100644
index 0000000..d77c869
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java
@@ -0,0 +1,71 @@
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Polygon;
+import java.util.List;
+
+/** Controller of a single Polygon on the map. */
+class PolygonController implements PolygonOptionsSink {
+  private final Polygon polygon;
+  private final String googleMapsPolygonId;
+  private boolean consumeTapEvents;
+
+  PolygonController(Polygon polygon, boolean consumeTapEvents) {
+    this.polygon = polygon;
+    this.consumeTapEvents = consumeTapEvents;
+    this.googleMapsPolygonId = polygon.getId();
+  }
+
+  void remove() {
+    polygon.remove();
+  }
+
+  @Override
+  public void setConsumeTapEvents(boolean consumeTapEvents) {
+    this.consumeTapEvents = consumeTapEvents;
+    polygon.setClickable(consumeTapEvents);
+  }
+
+  @Override
+  public void setFillColor(int color) {
+    polygon.setFillColor(color);
+  }
+
+  @Override
+  public void setStrokeColor(int color) {
+    polygon.setStrokeColor(color);
+  }
+
+  @Override
+  public void setGeodesic(boolean geodesic) {
+    polygon.setGeodesic(geodesic);
+  }
+
+  @Override
+  public void setPoints(List<LatLng> points) {
+    polygon.setPoints(points);
+  }
+
+  @Override
+  public void setVisible(boolean visible) {
+    polygon.setVisible(visible);
+  }
+
+  @Override
+  public void setStrokeWidth(float width) {
+    polygon.setStrokeWidth(width);
+  }
+
+  @Override
+  public void setZIndex(float zIndex) {
+    polygon.setZIndex(zIndex);
+  }
+
+  String getGoogleMapsPolygonId() {
+    return googleMapsPolygonId;
+  }
+
+  boolean consumeTapEvents() {
+    return consumeTapEvents;
+  }
+}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java
new file mode 100644
index 0000000..7abbcfa
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java
@@ -0,0 +1,24 @@
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.LatLng;
+import java.util.List;
+
+/** Receiver of Polygon configuration options. */
+interface PolygonOptionsSink {
+
+  void setConsumeTapEvents(boolean consumetapEvents);
+
+  void setFillColor(int color);
+
+  void setStrokeColor(int color);
+
+  void setGeodesic(boolean geodesic);
+
+  void setPoints(List<LatLng> points);
+
+  void setVisible(boolean visible);
+
+  void setStrokeWidth(float width);
+
+  void setZIndex(float zIndex);
+}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java
new file mode 100644
index 0000000..992e866
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java
@@ -0,0 +1,112 @@
+// 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.
+
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.Polygon;
+import com.google.android.gms.maps.model.PolygonOptions;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class PolygonsController {
+
+  private final Map<String, PolygonController> polygonIdToController;
+  private final Map<String, String> googleMapsPolygonIdToDartPolygonId;
+  private final MethodChannel methodChannel;
+  private GoogleMap googleMap;
+
+  PolygonsController(MethodChannel methodChannel) {
+    this.polygonIdToController = new HashMap<>();
+    this.googleMapsPolygonIdToDartPolygonId = new HashMap<>();
+    this.methodChannel = methodChannel;
+  }
+
+  void setGoogleMap(GoogleMap googleMap) {
+    this.googleMap = googleMap;
+  }
+
+  void addPolygons(List<Object> polygonsToAdd) {
+    if (polygonsToAdd != null) {
+      for (Object polygonToAdd : polygonsToAdd) {
+        addPolygon(polygonToAdd);
+      }
+    }
+  }
+
+  void changePolygons(List<Object> polygonsToChange) {
+    if (polygonsToChange != null) {
+      for (Object polygonToChange : polygonsToChange) {
+        changePolygon(polygonToChange);
+      }
+    }
+  }
+
+  void removePolygons(List<Object> polygonIdsToRemove) {
+    if (polygonIdsToRemove == null) {
+      return;
+    }
+    for (Object rawPolygonId : polygonIdsToRemove) {
+      if (rawPolygonId == null) {
+        continue;
+      }
+      String polygonId = (String) rawPolygonId;
+      final PolygonController polygonController = polygonIdToController.remove(polygonId);
+      if (polygonController != null) {
+        polygonController.remove();
+        googleMapsPolygonIdToDartPolygonId.remove(polygonController.getGoogleMapsPolygonId());
+      }
+    }
+  }
+
+  boolean onPolygonTap(String googlePolygonId) {
+    String polygonId = googleMapsPolygonIdToDartPolygonId.get(googlePolygonId);
+    if (polygonId == null) {
+      return false;
+    }
+    methodChannel.invokeMethod("polygon#onTap", Convert.polygonIdToJson(polygonId));
+    PolygonController polygonController = polygonIdToController.get(polygonId);
+    if (polygonController != null) {
+      return polygonController.consumeTapEvents();
+    }
+    return false;
+  }
+
+  private void addPolygon(Object polygon) {
+    if (polygon == null) {
+      return;
+    }
+    PolygonBuilder polygonBuilder = new PolygonBuilder();
+    String polygonId = Convert.interpretPolygonOptions(polygon, polygonBuilder);
+    PolygonOptions options = polygonBuilder.build();
+    addPolygon(polygonId, options, polygonBuilder.consumeTapEvents());
+  }
+
+  private void addPolygon(
+      String polygonId, PolygonOptions polygonOptions, boolean consumeTapEvents) {
+    final Polygon polygon = googleMap.addPolygon(polygonOptions);
+    PolygonController controller = new PolygonController(polygon, consumeTapEvents);
+    polygonIdToController.put(polygonId, controller);
+    googleMapsPolygonIdToDartPolygonId.put(polygon.getId(), polygonId);
+  }
+
+  private void changePolygon(Object polygon) {
+    if (polygon == null) {
+      return;
+    }
+    String polygonId = getPolygonId(polygon);
+    PolygonController polygonController = polygonIdToController.get(polygonId);
+    if (polygonController != null) {
+      Convert.interpretPolygonOptions(polygon, polygonController);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static String getPolygonId(Object polygon) {
+    Map<String, Object> polygonMap = (Map<String, Object>) polygon;
+    return (String) polygonMap.get("polygonId");
+  }
+}
diff --git a/packages/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/example/lib/main.dart
index 5a8d28c..1d17f13 100644
--- a/packages/google_maps_flutter/example/lib/main.dart
+++ b/packages/google_maps_flutter/example/lib/main.dart
@@ -12,6 +12,7 @@
 import 'page.dart';
 import 'place_circle.dart';
 import 'place_marker.dart';
+import 'place_polygon.dart';
 import 'place_polyline.dart';
 import 'scrolling_map.dart';
 
@@ -25,6 +26,7 @@
   MarkerIconsPage(),
   ScrollingMapPage(),
   PlacePolylinePage(),
+  PlacePolygonPage(),
   PlaceCirclePage(),
 ];
 
diff --git a/packages/google_maps_flutter/example/lib/place_polygon.dart b/packages/google_maps_flutter/example/lib/place_polygon.dart
new file mode 100644
index 0000000..1e48dfe
--- /dev/null
+++ b/packages/google_maps_flutter/example/lib/place_polygon.dart
@@ -0,0 +1,235 @@
+import 'package:flutter/material.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+import 'page.dart';
+
+class PlacePolygonPage extends Page {
+  PlacePolygonPage() : super(const Icon(Icons.linear_scale), 'Place polygon');
+
+  @override
+  Widget build(BuildContext context) {
+    return const PlacePolygonBody();
+  }
+}
+
+class PlacePolygonBody extends StatefulWidget {
+  const PlacePolygonBody();
+
+  @override
+  State<StatefulWidget> createState() => PlacePolygonBodyState();
+}
+
+class PlacePolygonBodyState extends State<PlacePolygonBody> {
+  PlacePolygonBodyState();
+
+  GoogleMapController controller;
+  Map<PolygonId, Polygon> polygons = <PolygonId, Polygon>{};
+  int _polygonIdCounter = 1;
+  PolygonId selectedPolygon;
+
+  // Values when toggling polygon color
+  int strokeColorsIndex = 0;
+  int fillColorsIndex = 0;
+  List<Color> colors = <Color>[
+    Colors.purple,
+    Colors.red,
+    Colors.green,
+    Colors.pink,
+  ];
+
+  // Values when toggling polygon width
+  int widthsIndex = 0;
+  List<int> widths = <int>[10, 20, 5];
+
+  void _onMapCreated(GoogleMapController controller) {
+    this.controller = controller;
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  void _onPolygonTapped(PolygonId polygonId) {
+    setState(() {
+      selectedPolygon = polygonId;
+    });
+  }
+
+  void _remove() {
+    setState(() {
+      if (polygons.containsKey(selectedPolygon)) {
+        polygons.remove(selectedPolygon);
+      }
+      selectedPolygon = null;
+    });
+  }
+
+  void _add() {
+    final int polygonCount = polygons.length;
+
+    if (polygonCount == 12) {
+      return;
+    }
+
+    final String polygonIdVal = 'polygon_id_$_polygonIdCounter';
+    _polygonIdCounter++;
+    final PolygonId polygonId = PolygonId(polygonIdVal);
+
+    final Polygon polygon = Polygon(
+      polygonId: polygonId,
+      consumeTapEvents: true,
+      strokeColor: Colors.orange,
+      strokeWidth: 5,
+      fillColor: Colors.green,
+      points: _createPoints(),
+      onTap: () {
+        _onPolygonTapped(polygonId);
+      },
+    );
+
+    setState(() {
+      polygons[polygonId] = polygon;
+    });
+  }
+
+  void _toggleGeodesic() {
+    final Polygon polygon = polygons[selectedPolygon];
+    setState(() {
+      polygons[selectedPolygon] = polygon.copyWith(
+        geodesicParam: !polygon.geodesic,
+      );
+    });
+  }
+
+  void _toggleVisible() {
+    final Polygon polygon = polygons[selectedPolygon];
+    setState(() {
+      polygons[selectedPolygon] = polygon.copyWith(
+        visibleParam: !polygon.visible,
+      );
+    });
+  }
+
+  void _changeStrokeColor() {
+    final Polygon polygon = polygons[selectedPolygon];
+    setState(() {
+      polygons[selectedPolygon] = polygon.copyWith(
+        strokeColorParam: colors[++strokeColorsIndex % colors.length],
+      );
+    });
+  }
+
+  void _changeFillColor() {
+    final Polygon polygon = polygons[selectedPolygon];
+    setState(() {
+      polygons[selectedPolygon] = polygon.copyWith(
+        fillColorParam: colors[++fillColorsIndex % colors.length],
+      );
+    });
+  }
+
+  void _changeWidth() {
+    final Polygon polygon = polygons[selectedPolygon];
+    setState(() {
+      polygons[selectedPolygon] = polygon.copyWith(
+        strokeWidthParam: widths[++widthsIndex % widths.length],
+      );
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: <Widget>[
+        Center(
+          child: SizedBox(
+            width: 350.0,
+            height: 300.0,
+            child: GoogleMap(
+              initialCameraPosition: const CameraPosition(
+                target: LatLng(52.4478, -3.5402),
+                zoom: 7.0,
+              ),
+              polygons: Set<Polygon>.of(polygons.values),
+              onMapCreated: _onMapCreated,
+            ),
+          ),
+        ),
+        Expanded(
+          child: SingleChildScrollView(
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+              children: <Widget>[
+                Row(
+                  children: <Widget>[
+                    Column(
+                      children: <Widget>[
+                        FlatButton(
+                          child: const Text('add'),
+                          onPressed: _add,
+                        ),
+                        FlatButton(
+                          child: const Text('remove'),
+                          onPressed: (selectedPolygon == null) ? null : _remove,
+                        ),
+                        FlatButton(
+                          child: const Text('toggle visible'),
+                          onPressed:
+                              (selectedPolygon == null) ? null : _toggleVisible,
+                        ),
+                        FlatButton(
+                          child: const Text('toggle geodesic'),
+                          onPressed: (selectedPolygon == null)
+                              ? null
+                              : _toggleGeodesic,
+                        ),
+                      ],
+                    ),
+                    Column(
+                      children: <Widget>[
+                        FlatButton(
+                          child: const Text('change stroke width'),
+                          onPressed:
+                              (selectedPolygon == null) ? null : _changeWidth,
+                        ),
+                        FlatButton(
+                          child: const Text('change stroke color'),
+                          onPressed: (selectedPolygon == null)
+                              ? null
+                              : _changeStrokeColor,
+                        ),
+                        FlatButton(
+                          child: const Text('change fill color'),
+                          onPressed: (selectedPolygon == null)
+                              ? null
+                              : _changeFillColor,
+                        ),
+                      ],
+                    )
+                  ],
+                )
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  List<LatLng> _createPoints() {
+    final List<LatLng> points = <LatLng>[];
+    final double offset = _polygonIdCounter.ceilToDouble();
+    points.add(_createLatLng(51.2395 + offset, -3.4314));
+    points.add(_createLatLng(53.5234 + offset, -3.5314));
+    points.add(_createLatLng(52.4351 + offset, -4.5235));
+    points.add(_createLatLng(52.1231 + offset, -5.0829));
+    return points;
+  }
+
+  LatLng _createLatLng(double lat, double lng) {
+    return LatLng(lat, lng);
+  }
+}
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
index 60e1d42..8c39537 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
@@ -6,6 +6,7 @@
 #import <GoogleMaps/GoogleMaps.h>
 #import "GoogleMapCircleController.h"
 #import "GoogleMapMarkerController.h"
+#import "GoogleMapPolygonController.h"
 #import "GoogleMapPolylineController.h"
 
 // Defines map UI options writable from Flutter.
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
index 5dd1d25..fa49638 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
@@ -54,6 +54,7 @@
   // https://github.com/flutter/flutter/issues/27550
   BOOL _cameraDidInitialSetup;
   FLTMarkersController* _markersController;
+  FLTPolygonsController* _polygonsController;
   FLTPolylinesController* _polylinesController;
   FLTCirclesController* _circlesController;
 }
@@ -86,6 +87,9 @@
     _markersController = [[FLTMarkersController alloc] init:_channel
                                                     mapView:_mapView
                                                   registrar:registrar];
+    _polygonsController = [[FLTPolygonsController alloc] init:_channel
+                                                      mapView:_mapView
+                                                    registrar:registrar];
     _polylinesController = [[FLTPolylinesController alloc] init:_channel
                                                         mapView:_mapView
                                                       registrar:registrar];
@@ -96,6 +100,10 @@
     if ([markersToAdd isKindOfClass:[NSArray class]]) {
       [_markersController addMarkers:markersToAdd];
     }
+    id polygonsToAdd = args[@"polygonToAdd"];
+    if ([polygonsToAdd isKindOfClass:[NSArray class]]) {
+      [_polygonsController addPolygons:polygonsToAdd];
+    }
     id polylinesToAdd = args[@"polylinesToAdd"];
     if ([polylinesToAdd isKindOfClass:[NSArray class]]) {
       [_polylinesController addPolylines:polylinesToAdd];
@@ -155,6 +163,20 @@
       [_markersController removeMarkerIds:markerIdsToRemove];
     }
     result(nil);
+  } else if ([call.method isEqualToString:@"polygons#update"]) {
+    id polygonsToAdd = call.arguments[@"polygonsToAdd"];
+    if ([polygonsToAdd isKindOfClass:[NSArray class]]) {
+      [_polygonsController addPolygons:polygonsToAdd];
+    }
+    id polygonsToChange = call.arguments[@"polygonsToChange"];
+    if ([polygonsToChange isKindOfClass:[NSArray class]]) {
+      [_polygonsController changePolygons:polygonsToChange];
+    }
+    id polygonIdsToRemove = call.arguments[@"polygonIdsToRemove"];
+    if ([polygonIdsToRemove isKindOfClass:[NSArray class]]) {
+      [_polygonsController removePolygonIds:polygonIdsToRemove];
+    }
+    result(nil);
   } else if ([call.method isEqualToString:@"polylines#update"]) {
     id polylinesToAdd = call.arguments[@"polylinesToAdd"];
     if ([polylinesToAdd isKindOfClass:[NSArray class]]) {
@@ -325,6 +347,8 @@
   NSString* overlayId = overlay.userData[0];
   if ([_polylinesController hasPolylineWithId:overlayId]) {
     [_polylinesController onPolylineTap:overlayId];
+  } else if ([_polygonsController hasPolygonWithId:overlayId]) {
+    [_polygonsController onPolygonTap:overlayId];
   } else if ([_circlesController hasCircleWithId:overlayId]) {
     [_circlesController onCircleTap:overlayId];
   }
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h
new file mode 100644
index 0000000..c7613fd
--- /dev/null
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h
@@ -0,0 +1,37 @@
+// 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 <Flutter/Flutter.h>
+#import <GoogleMaps/GoogleMaps.h>
+
+// Defines polygon UI options writable from Flutter.
+@protocol FLTGoogleMapPolygonOptionsSink
+- (void)setConsumeTapEvents:(BOOL)consume;
+- (void)setVisible:(BOOL)visible;
+- (void)setFillColor:(UIColor*)color;
+- (void)setStrokeColor:(UIColor*)color;
+- (void)setStrokeWidth:(CGFloat)width;
+- (void)setPoints:(NSArray<CLLocation*>*)points;
+- (void)setZIndex:(int)zIndex;
+@end
+
+// Defines polygon controllable by Flutter.
+@interface FLTGoogleMapPolygonController : NSObject <FLTGoogleMapPolygonOptionsSink>
+@property(atomic, readonly) NSString* polygonId;
+- (instancetype)initPolygonWithPath:(GMSMutablePath*)path
+                          polygonId:(NSString*)polygonId
+                            mapView:(GMSMapView*)mapView;
+- (void)removePolygon;
+@end
+
+@interface FLTPolygonsController : NSObject
+- (instancetype)init:(FlutterMethodChannel*)methodChannel
+             mapView:(GMSMapView*)mapView
+           registrar:(NSObject<FlutterPluginRegistrar>*)registrar;
+- (void)addPolygons:(NSArray*)polygonsToAdd;
+- (void)changePolygons:(NSArray*)polygonsToChange;
+- (void)removePolygonIds:(NSArray*)polygonIdsToRemove;
+- (void)onPolygonTap:(NSString*)polygonId;
+- (bool)hasPolygonWithId:(NSString*)polygonId;
+@end
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m
new file mode 100644
index 0000000..4bc4f17
--- /dev/null
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m
@@ -0,0 +1,189 @@
+// 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 "GoogleMapPolygonController.h"
+#import "JsonConversions.h"
+
+@implementation FLTGoogleMapPolygonController {
+  GMSPolygon* _polygon;
+  GMSMapView* _mapView;
+}
+- (instancetype)initPolygonWithPath:(GMSMutablePath*)path
+                          polygonId:(NSString*)polygonId
+                            mapView:(GMSMapView*)mapView {
+  self = [super init];
+  if (self) {
+    _polygon = [GMSPolygon polygonWithPath:path];
+    _mapView = mapView;
+    _polygonId = polygonId;
+    _polygon.userData = @[ polygonId ];
+  }
+  return self;
+}
+
+- (void)removePolygon {
+  _polygon.map = nil;
+}
+
+#pragma mark - FLTGoogleMapPolygonOptionsSink methods
+
+- (void)setConsumeTapEvents:(BOOL)consumes {
+  _polygon.tappable = consumes;
+}
+- (void)setVisible:(BOOL)visible {
+  _polygon.map = visible ? _mapView : nil;
+}
+- (void)setZIndex:(int)zIndex {
+  _polygon.zIndex = zIndex;
+}
+- (void)setPoints:(NSArray<CLLocation*>*)points {
+  GMSMutablePath* path = [GMSMutablePath path];
+
+  for (CLLocation* location in points) {
+    [path addCoordinate:location.coordinate];
+  }
+  _polygon.path = path;
+}
+
+- (void)setFillColor:(UIColor*)color {
+  _polygon.fillColor = color;
+}
+- (void)setStrokeColor:(UIColor*)color {
+  _polygon.strokeColor = color;
+}
+- (void)setStrokeWidth:(CGFloat)width {
+  _polygon.strokeWidth = width;
+}
+@end
+
+static int ToInt(NSNumber* data) { return [FLTGoogleMapJsonConversions toInt:data]; }
+
+static BOOL ToBool(NSNumber* data) { return [FLTGoogleMapJsonConversions toBool:data]; }
+
+static NSArray<CLLocation*>* ToPoints(NSArray* data) {
+  return [FLTGoogleMapJsonConversions toPoints:data];
+}
+
+static UIColor* ToColor(NSNumber* data) { return [FLTGoogleMapJsonConversions toColor:data]; }
+
+static void InterpretPolygonOptions(NSDictionary* data, id<FLTGoogleMapPolygonOptionsSink> sink,
+                                    NSObject<FlutterPluginRegistrar>* registrar) {
+  NSNumber* consumeTapEvents = data[@"consumeTapEvents"];
+  if (consumeTapEvents) {
+    [sink setConsumeTapEvents:ToBool(consumeTapEvents)];
+  }
+
+  NSNumber* visible = data[@"visible"];
+  if (visible) {
+    [sink setVisible:ToBool(visible)];
+  }
+
+  NSNumber* zIndex = data[@"zIndex"];
+  if (zIndex) {
+    [sink setZIndex:ToInt(zIndex)];
+  }
+
+  NSArray* points = data[@"points"];
+  if (points) {
+    [sink setPoints:ToPoints(points)];
+  }
+
+  NSNumber* fillColor = data[@"fillColor"];
+  if (fillColor) {
+    [sink setFillColor:ToColor(fillColor)];
+  }
+
+  NSNumber* strokeColor = data[@"strokeColor"];
+  if (strokeColor) {
+    [sink setStrokeColor:ToColor(strokeColor)];
+  }
+
+  NSNumber* strokeWidth = data[@"strokeWidth"];
+  if (strokeWidth) {
+    [sink setStrokeWidth:ToInt(strokeWidth)];
+  }
+}
+
+@implementation FLTPolygonsController {
+  NSMutableDictionary* _polygonIdToController;
+  FlutterMethodChannel* _methodChannel;
+  NSObject<FlutterPluginRegistrar>* _registrar;
+  GMSMapView* _mapView;
+}
+- (instancetype)init:(FlutterMethodChannel*)methodChannel
+             mapView:(GMSMapView*)mapView
+           registrar:(NSObject<FlutterPluginRegistrar>*)registrar {
+  self = [super init];
+  if (self) {
+    _methodChannel = methodChannel;
+    _mapView = mapView;
+    _polygonIdToController = [NSMutableDictionary dictionaryWithCapacity:1];
+    _registrar = registrar;
+  }
+  return self;
+}
+- (void)addPolygons:(NSArray*)polygonsToAdd {
+  for (NSDictionary* polygon in polygonsToAdd) {
+    GMSMutablePath* path = [FLTPolygonsController getPath:polygon];
+    NSString* polygonId = [FLTPolygonsController getPolygonId:polygon];
+    FLTGoogleMapPolygonController* controller =
+        [[FLTGoogleMapPolygonController alloc] initPolygonWithPath:path
+                                                         polygonId:polygonId
+                                                           mapView:_mapView];
+    InterpretPolygonOptions(polygon, controller, _registrar);
+    _polygonIdToController[polygonId] = controller;
+  }
+}
+- (void)changePolygons:(NSArray*)polygonsToChange {
+  for (NSDictionary* polygon in polygonsToChange) {
+    NSString* polygonId = [FLTPolygonsController getPolygonId:polygon];
+    FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId];
+    if (!controller) {
+      continue;
+    }
+    InterpretPolygonOptions(polygon, controller, _registrar);
+  }
+}
+- (void)removePolygonIds:(NSArray*)polygonIdsToRemove {
+  for (NSString* polygonId in polygonIdsToRemove) {
+    if (!polygonId) {
+      continue;
+    }
+    FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId];
+    if (!controller) {
+      continue;
+    }
+    [controller removePolygon];
+    [_polygonIdToController removeObjectForKey:polygonId];
+  }
+}
+- (void)onPolygonTap:(NSString*)polygonId {
+  if (!polygonId) {
+    return;
+  }
+  FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId];
+  if (!controller) {
+    return;
+  }
+  [_methodChannel invokeMethod:@"polygon#onTap" arguments:@{@"polygonId" : polygonId}];
+}
+- (bool)hasPolygonWithId:(NSString*)polygonId {
+  if (!polygonId) {
+    return false;
+  }
+  return _polygonIdToController[polygonId] != nil;
+}
++ (GMSMutablePath*)getPath:(NSDictionary*)polygon {
+  NSArray* pointArray = polygon[@"points"];
+  NSArray<CLLocation*>* points = ToPoints(pointArray);
+  GMSMutablePath* path = [GMSMutablePath path];
+  for (CLLocation* location in points) {
+    [path addCoordinate:location.coordinate];
+  }
+  return path;
+}
++ (NSString*)getPolygonId:(NSDictionary*)polygon {
+  return polygon[@"polygonId"];
+}
+@end
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h
index 5b89429..645ace3 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h
@@ -7,6 +7,7 @@
 #import "GoogleMapCircleController.h"
 #import "GoogleMapController.h"
 #import "GoogleMapMarkerController.h"
+#import "GoogleMapPolygonController.h"
 #import "GoogleMapPolylineController.h"
 
 @interface FLTGoogleMapsPlugin : NSObject <FlutterPlugin>
diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart
index 14adf45..91f0371 100644
--- a/packages/google_maps_flutter/lib/google_maps_flutter.dart
+++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart
@@ -24,6 +24,8 @@
 part 'src/marker_updates.dart';
 part 'src/location.dart';
 part 'src/pattern_item.dart';
+part 'src/polygon.dart';
+part 'src/polygon_updates.dart';
 part 'src/polyline.dart';
 part 'src/polyline_updates.dart';
 part 'src/circle.dart';
diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart
index 5cc4065..b3f3990 100644
--- a/packages/google_maps_flutter/lib/src/controller.dart
+++ b/packages/google_maps_flutter/lib/src/controller.dart
@@ -66,6 +66,9 @@
       case 'polyline#onTap':
         _googleMapState.onPolylineTap(call.arguments['polylineId']);
         break;
+      case 'polygon#onTap':
+        _googleMapState.onPolygonTap(call.arguments['polygonId']);
+        break;
       case 'circle#onTap':
         _googleMapState.onCircleTap(call.arguments['circleId']);
         break;
@@ -117,6 +120,23 @@
     );
   }
 
+  /// 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.
+  Future<void> _updatePolygons(_PolygonUpdates polygonUpdates) async {
+    assert(polygonUpdates != null);
+    // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
+    // https://github.com/flutter/flutter/issues/26431
+    // ignore: strong_mode_implicit_dynamic_method
+    await channel.invokeMethod(
+      'polygons#update',
+      polygonUpdates._toMap(),
+    );
+  }
+
   /// Updates polyline configuration.
   ///
   /// Change listeners are notified once the update has been made on the
diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart
index 2a2562f..775b94a 100644
--- a/packages/google_maps_flutter/lib/src/google_map.dart
+++ b/packages/google_maps_flutter/lib/src/google_map.dart
@@ -31,6 +31,7 @@
     this.myLocationEnabled = false,
     this.myLocationButtonEnabled = true,
     this.markers,
+    this.polygons,
     this.polylines,
     this.circles,
     this.onCameraMoveStarted,
@@ -75,6 +76,9 @@
   /// Markers to be placed on the map.
   final Set<Marker> markers;
 
+  /// Polygons to be placed on the map.
+  final Set<Polygon> polygons;
+
   /// Polylines to be placed on the map.
   final Set<Polyline> polylines;
 
@@ -166,6 +170,7 @@
       Completer<GoogleMapController>();
 
   Map<MarkerId, Marker> _markers = <MarkerId, Marker>{};
+  Map<PolygonId, Polygon> _polygons = <PolygonId, Polygon>{};
   Map<PolylineId, Polyline> _polylines = <PolylineId, Polyline>{};
   Map<CircleId, Circle> _circles = <CircleId, Circle>{};
   _GoogleMapOptions _googleMapOptions;
@@ -176,6 +181,7 @@
       'initialCameraPosition': widget.initialCameraPosition?._toMap(),
       'options': _googleMapOptions.toMap(),
       'markersToAdd': _serializeMarkerSet(widget.markers),
+      'polygonsToAdd': _serializePolygonSet(widget.polygons),
       'polylinesToAdd': _serializePolylineSet(widget.polylines),
       'circlesToAdd': _serializeCircleSet(widget.circles),
     };
@@ -206,6 +212,7 @@
     super.initState();
     _googleMapOptions = _GoogleMapOptions.fromWidget(widget);
     _markers = _keyByMarkerId(widget.markers);
+    _polygons = _keyByPolygonId(widget.polygons);
     _polylines = _keyByPolylineId(widget.polylines);
     _circles = _keyByCircleId(widget.circles);
   }
@@ -215,6 +222,7 @@
     super.didUpdateWidget(oldWidget);
     _updateOptions();
     _updateMarkers();
+    _updatePolygons();
     _updatePolylines();
     _updateCircles();
   }
@@ -238,6 +246,13 @@
     _markers = _keyByMarkerId(widget.markers);
   }
 
+  void _updatePolygons() async {
+    final GoogleMapController controller = await _controller.future;
+    controller._updatePolygons(
+        _PolygonUpdates.from(_polygons.values.toSet(), widget.polygons));
+    _polygons = _keyByPolygonId(widget.polygons);
+  }
+
   void _updatePolylines() async {
     final GoogleMapController controller = await _controller.future;
     controller._updatePolylines(
@@ -272,6 +287,12 @@
     }
   }
 
+  void onPolygonTap(String polygonIdParam) {
+    assert(polygonIdParam != null);
+    final PolygonId polygonId = PolygonId(polygonIdParam);
+    _polygons[polygonId].onTap();
+  }
+
   void onPolylineTap(String polylineIdParam) {
     assert(polylineIdParam != null);
     final PolylineId polylineId = PolylineId(polylineIdParam);
diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart
new file mode 100644
index 0000000..439a5f5
--- /dev/null
+++ b/packages/google_maps_flutter/lib/src/polygon.dart
@@ -0,0 +1,179 @@
+part of google_maps_flutter;
+
+/// Uniquely identifies a [Polygon] among [GoogleMap] polygons.
+///
+/// This does not have to be globally unique, only unique among the list.
+@immutable
+class PolygonId {
+  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}';
+  }
+}
+
+/// Draws a polygon through geographical locations on the map.
+@immutable
+class Polygon {
+  const Polygon({
+    @required this.polygonId,
+    this.consumeTapEvents = false,
+    this.fillColor = Colors.black,
+    this.geodesic = false,
+    this.points = const <LatLng>[],
+    this.strokeColor = Colors.black,
+    this.strokeWidth = 10,
+    this.visible = true,
+    this.zIndex = 0,
+    this.onTap,
+  });
+
+  /// Uniquely identifies a [Polygon].
+  final PolygonId polygonId;
+
+  /// True if the [Polygon] consumes tap events.
+  ///
+  /// If this is false, [onTap] callback will not be triggered.
+  final bool consumeTapEvents;
+
+  /// Fill color in ARGB format, the same format used by Color. The default value is black (0xff000000).
+  final Color fillColor;
+
+  /// Indicates whether the segments of the polygon should be drawn as geodesics, as opposed to straight lines
+  /// on the Mercator projection.
+  ///
+  /// A geodesic is the shortest path between two points on the Earth's surface.
+  /// The geodesic curve is constructed assuming the Earth is a sphere
+  final bool geodesic;
+
+  /// The vertices of the polygon to be drawn.
+  ///
+  /// Line segments are drawn between consecutive points. A polygon is not closed by
+  /// default; to form a closed polygon, the start and end points must be the same.
+  final List<LatLng> points;
+
+  /// True if the marker is visible.
+  final bool visible;
+
+  /// Line color in ARGB format, the same format used by Color. The default value is black (0xff000000).
+  final Color strokeColor;
+
+  /// Width of the polygon, used to define the width of the line to be drawn.
+  ///
+  /// The width is constant and independent of the camera's zoom level.
+  /// The default value is 10.
+  final int strokeWidth;
+
+  /// The z-index of the polygon, used to determine relative drawing order of
+  /// map overlays.
+  ///
+  /// Overlays are drawn in order of z-index, so that lower values means drawn
+  /// earlier, and thus appearing to be closer to the surface of the Earth.
+  final int zIndex;
+
+  /// Callbacks to receive tap events for polygon placed on this map.
+  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,
+    Color strokeColorParam,
+    int strokeWidthParam,
+    bool visibleParam,
+    int zIndexParam,
+    VoidCallback onTapParam,
+  }) {
+    return Polygon(
+      polygonId: polygonId,
+      consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents,
+      fillColor: fillColorParam ?? fillColor,
+      geodesic: geodesicParam ?? geodesic,
+      points: pointsParam ?? points,
+      strokeColor: strokeColorParam ?? strokeColor,
+      strokeWidth: strokeWidthParam ?? strokeWidth,
+      visible: visibleParam ?? visible,
+      onTap: onTapParam ?? onTap,
+      zIndex: zIndexParam ?? zIndex,
+    );
+  }
+
+  dynamic _toJson() {
+    final Map<String, dynamic> json = <String, dynamic>{};
+
+    void addIfPresent(String fieldName, dynamic value) {
+      if (value != null) {
+        json[fieldName] = value;
+      }
+    }
+
+    addIfPresent('polygonId', polygonId.value);
+    addIfPresent('consumeTapEvents', consumeTapEvents);
+    addIfPresent('fillColor', fillColor.value);
+    addIfPresent('geodesic', geodesic);
+    addIfPresent('strokeColor', strokeColor.value);
+    addIfPresent('strokeWidth', strokeWidth);
+    addIfPresent('visible', visible);
+    addIfPresent('zIndex', zIndex);
+
+    if (points != null) {
+      json['points'] = _pointsToJson();
+    }
+
+    return json;
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    if (other.runtimeType != runtimeType) return false;
+    final Polygon typedOther = other;
+    return polygonId == typedOther.polygonId;
+  }
+
+  @override
+  int get hashCode => polygonId.hashCode;
+
+  dynamic _pointsToJson() {
+    final List<dynamic> result = <dynamic>[];
+    for (final LatLng point in points) {
+      result.add(point._toJson());
+    }
+    return result;
+  }
+}
+
+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)));
+}
+
+List<Map<String, dynamic>> _serializePolygonSet(Set<Polygon> polygons) {
+  if (polygons == null) {
+    return null;
+  }
+  return polygons
+      .map<Map<String, dynamic>>((Polygon p) => p._toJson())
+      .toList();
+}
diff --git a/packages/google_maps_flutter/lib/src/polygon_updates.dart b/packages/google_maps_flutter/lib/src/polygon_updates.dart
new file mode 100644
index 0000000..c7a04f4
--- /dev/null
+++ b/packages/google_maps_flutter/lib/src/polygon_updates.dart
@@ -0,0 +1,90 @@
+// 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.
+
+part of google_maps_flutter;
+
+/// [Polygon] update events to be applied to the [GoogleMap].
+///
+/// Used in [GoogleMapController] when the map is updated.
+class _PolygonUpdates {
+  /// 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();
+
+    final Set<Polygon> _polygonsToChange = currentPolygonIds
+        .intersection(prevPolygonIds)
+        .map(idToCurrentPolygon)
+        .toSet();
+
+    polygonsToAdd = _polygonsToAdd;
+    polygonIdsToRemove = _polygonIdsToRemove;
+    polygonsToChange = _polygonsToChange;
+  }
+
+  Set<Polygon> polygonsToAdd;
+  Set<PolygonId> polygonIdsToRemove;
+  Set<Polygon> polygonsToChange;
+
+  Map<String, dynamic> _toMap() {
+    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}';
+  }
+}
diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml
index ecf9467..9af1da6 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.14+1
+version: 0.5.15
 
 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 805fa7a..f9dd2c7 100644
--- a/packages/google_maps_flutter/test/fake_maps_controllers.dart
+++ b/packages/google_maps_flutter/test/fake_maps_controllers.dart
@@ -16,6 +16,7 @@
     channel.setMockMethodCallHandler(onMethodCall);
     updateOptions(params['options']);
     updateMarkers(params);
+    updatePolygons(params);
     updatePolylines(params);
     updateCircles(params);
   }
@@ -52,6 +53,12 @@
 
   Set<Marker> markersToChange;
 
+  Set<PolygonId> polygonIdsToRemove;
+
+  Set<Polygon> polygonsToAdd;
+
+  Set<Polygon> polygonsToChange;
+
   Set<PolylineId> polylineIdsToRemove;
 
   Set<Polyline> polylinesToAdd;
@@ -72,6 +79,9 @@
       case 'markers#update':
         updateMarkers(call.arguments);
         return Future<void>.sync(() {});
+      case 'polygons#update':
+        updatePolygons(call.arguments);
+        return Future<void>.sync(() {});
       case 'polylines#update':
         updatePolylines(call.arguments);
         return Future<void>.sync(() {});
@@ -141,6 +151,53 @@
     return result;
   }
 
+  void updatePolygons(Map<dynamic, dynamic> polygonUpdates) {
+    if (polygonUpdates == null) {
+      return;
+    }
+    polygonsToAdd = _deserializePolygons(polygonUpdates['polygonsToAdd']);
+    polygonIdsToRemove =
+        _deserializePolygonIds(polygonUpdates['polygonIdsToRemove']);
+    polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']);
+  }
+
+  Set<PolygonId> _deserializePolygonIds(List<dynamic> polygonIds) {
+    if (polygonIds == null) {
+      // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+      // https://github.com/flutter/flutter/issues/28312
+      // ignore: prefer_collection_literals
+      return Set<PolygonId>();
+    }
+    return polygonIds.map((dynamic polygonId) => PolygonId(polygonId)).toSet();
+  }
+
+  Set<Polygon> _deserializePolygons(dynamic polygons) {
+    if (polygons == null) {
+      // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+      // https://github.com/flutter/flutter/issues/28312
+      // ignore: prefer_collection_literals
+      return Set<Polygon>();
+    }
+    final List<dynamic> polygonsData = polygons;
+    // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+    // https://github.com/flutter/flutter/issues/28312
+    // ignore: prefer_collection_literals
+    final Set<Polygon> result = Set<Polygon>();
+    for (Map<dynamic, dynamic> polygonData in polygonsData) {
+      final String polygonId = polygonData['polygonId'];
+      final bool visible = polygonData['visible'];
+      final bool geodesic = polygonData['geodesic'];
+
+      result.add(Polygon(
+        polygonId: PolygonId(polygonId),
+        visible: visible,
+        geodesic: geodesic,
+      ));
+    }
+
+    return result;
+  }
+
   void updatePolylines(Map<dynamic, dynamic> polylineUpdates) {
     if (polylineUpdates == null) {
       return;
diff --git a/packages/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/test/polygon_updates_test.dart
new file mode 100644
index 0000000..f7b2c1c
--- /dev/null
+++ b/packages/google_maps_flutter/test/polygon_updates_test.dart
@@ -0,0 +1,200 @@
+// 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 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+import 'fake_maps_controllers.dart';
+
+Set<Polygon> _toSet({Polygon p1, Polygon p2, Polygon p3}) {
+  final Set<Polygon> res = Set<Polygon>.identity();
+  if (p1 != null) {
+    res.add(p1);
+  }
+  if (p2 != null) {
+    res.add(p2);
+  }
+  if (p3 != null) {
+    res.add(p3);
+  }
+  return res;
+}
+
+Widget _mapWithPolygons(Set<Polygon> polygons) {
+  return Directionality(
+    textDirection: TextDirection.ltr,
+    child: GoogleMap(
+      initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)),
+      polygons: polygons,
+    ),
+  );
+}
+
+void main() {
+  final FakePlatformViewsController fakePlatformViewsController =
+      FakePlatformViewsController();
+
+  setUpAll(() {
+    SystemChannels.platform_views.setMockMethodCallHandler(
+        fakePlatformViewsController.fakePlatformViewsMethodHandler);
+  });
+
+  setUp(() {
+    fakePlatformViewsController.reset();
+  });
+
+  testWidgets('Initializing a polygon', (WidgetTester tester) async {
+    final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.polygonsToAdd.length, 1);
+
+    final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first;
+    expect(initializedPolygon, equals(p1));
+    expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.polygonsToChange.isEmpty, true);
+  });
+
+  testWidgets("Adding a polygon", (WidgetTester tester) async {
+    final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+    final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1, p2: p2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.polygonsToAdd.length, 1);
+
+    final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first;
+    expect(addedPolygon, equals(p2));
+    expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+
+    expect(platformGoogleMap.polygonsToChange.length, 1);
+    expect(platformGoogleMap.polygonsToChange.first, equals(p1));
+  });
+
+  testWidgets("Removing a polygon", (WidgetTester tester) async {
+    final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+    await tester.pumpWidget(_mapWithPolygons(null));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.polygonIdsToRemove.length, 1);
+    expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId));
+
+    expect(platformGoogleMap.polygonsToChange.isEmpty, true);
+    expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+  });
+
+  testWidgets("Updating a polygon", (WidgetTester tester) async {
+    final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+    final Polygon p2 =
+        Polygon(polygonId: PolygonId("polygon_1"), geodesic: true);
+
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.polygonsToChange.length, 1);
+    expect(platformGoogleMap.polygonsToChange.first, equals(p2));
+
+    expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+  });
+
+  testWidgets("Updating a polygon", (WidgetTester tester) async {
+    final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+    final Polygon p2 =
+        Polygon(polygonId: PolygonId("polygon_1"), geodesic: true);
+
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+    await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.polygonsToChange.length, 1);
+
+    final Polygon update = platformGoogleMap.polygonsToChange.first;
+    expect(update, equals(p2));
+    expect(update.geodesic, true);
+  });
+
+  testWidgets("Multi Update", (WidgetTester tester) async {
+    Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+    Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+    final Set<Polygon> prev = _toSet(p1: p1, p2: p2);
+    p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false);
+    p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true);
+    final Set<Polygon> cur = _toSet(p1: p1, p2: p2);
+
+    await tester.pumpWidget(_mapWithPolygons(prev));
+    await tester.pumpWidget(_mapWithPolygons(cur));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.polygonsToChange, cur);
+    expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+  });
+
+  testWidgets("Multi Update", (WidgetTester tester) async {
+    Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+    final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3"));
+    final Set<Polygon> prev = _toSet(p2: p2, p3: p3);
+
+    // p1 is added, p2 is updated, p3 is removed.
+    final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+    p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true);
+    final Set<Polygon> cur = _toSet(p1: p1, p2: p2);
+
+    await tester.pumpWidget(_mapWithPolygons(prev));
+    await tester.pumpWidget(_mapWithPolygons(cur));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.polygonsToChange.length, 1);
+    expect(platformGoogleMap.polygonsToAdd.length, 1);
+    expect(platformGoogleMap.polygonIdsToRemove.length, 1);
+
+    expect(platformGoogleMap.polygonsToChange.first, equals(p2));
+    expect(platformGoogleMap.polygonsToAdd.first, equals(p1));
+    expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId));
+  });
+
+  testWidgets(
+    "Partial Update",
+    (WidgetTester tester) async {
+      final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+      Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+      final Set<Polygon> prev = _toSet(p1: p1, p2: p2);
+      p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true);
+      final Set<Polygon> cur = _toSet(p1: p1, p2: p2);
+
+      await tester.pumpWidget(_mapWithPolygons(prev));
+      await tester.pumpWidget(_mapWithPolygons(cur));
+
+      final FakePlatformGoogleMap platformGoogleMap =
+          fakePlatformViewsController.lastCreatedView;
+
+      expect(platformGoogleMap.polygonsToChange, _toSet(p2: p2));
+      expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+      expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+    },
+    // The test is currently broken due to a bug (we're updating all polygons
+    // instead of just the ones that were changed):
+    // https://github.com/flutter/flutter/issues/30764
+    // TODO(amirh): enable this test when the issue is fixed.
+    skip: true,
+  );
+}