[google_maps_flutter] add tile overlays (#3434)

diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle
index a1d7da0..479c100 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle
+++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle
@@ -39,6 +39,11 @@
         androidTestImplementation 'androidx.test:rules:1.2.0'
         androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
     }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
index 4108a1d..f9e0ed9 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
@@ -23,6 +23,7 @@
 import com.google.android.gms.maps.model.PatternItem;
 import com.google.android.gms.maps.model.RoundCap;
 import com.google.android.gms.maps.model.SquareCap;
+import com.google.android.gms.maps.model.Tile;
 import io.flutter.view.FlutterMain;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -78,7 +79,8 @@
       }
     } else {
       throw new IllegalArgumentException(
-          "fromBytes should have exactly one argument, the bytes. Got: " + data.size());
+          "fromBytes should have exactly one argument, interpretTileOverlayOptions the bytes. Got: "
+              + data.size());
     }
   }
 
@@ -200,6 +202,20 @@
     return data;
   }
 
+  static Map<String, Object> tileOverlayArgumentsToJson(
+      String tileOverlayId, int x, int y, int zoom) {
+
+    if (tileOverlayId == null) {
+      return null;
+    }
+    final Map<String, Object> data = new HashMap<>(4);
+    data.put("tileOverlayId", tileOverlayId);
+    data.put("x", x);
+    data.put("y", y);
+    data.put("zoom", zoom);
+    return data;
+  }
+
   static Object latLngToJson(LatLng latLng) {
     return Arrays.asList(latLng.latitude, latLng.longitude);
   }
@@ -645,4 +661,39 @@
         throw new IllegalArgumentException("Cannot interpret " + o + " as Cap");
     }
   }
+
+  static String interpretTileOverlayOptions(Map<String, ?> data, TileOverlaySink sink) {
+    final Object fadeIn = data.get("fadeIn");
+    if (fadeIn != null) {
+      sink.setFadeIn(toBoolean(fadeIn));
+    }
+    final Object transparency = data.get("transparency");
+    if (transparency != null) {
+      sink.setTransparency(toFloat(transparency));
+    }
+    final Object zIndex = data.get("zIndex");
+    if (zIndex != null) {
+      sink.setZIndex(toFloat(zIndex));
+    }
+    final Object visible = data.get("visible");
+    if (visible != null) {
+      sink.setVisible(toBoolean(visible));
+    }
+    final String tileOverlayId = (String) data.get("tileOverlayId");
+    if (tileOverlayId == null) {
+      throw new IllegalArgumentException("tileOverlayId was null");
+    } else {
+      return tileOverlayId;
+    }
+  }
+
+  static Tile interpretTile(Map<String, ?> data) {
+    int width = toInt(data.get("width"));
+    int height = toInt(data.get("height"));
+    byte[] dataArray = null;
+    if (data.get("data") != null) {
+      dataArray = (byte[]) data.get("data");
+    }
+    return new Tile(width, height, dataArray);
+  }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
index 93a3c3e..6d5c8c6 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
@@ -10,6 +10,8 @@
 import com.google.android.gms.maps.model.CameraPosition;
 import com.google.android.gms.maps.model.LatLngBounds;
 import io.flutter.plugin.common.BinaryMessenger;
+import java.util.List;
+import java.util.Map;
 
 class GoogleMapBuilder implements GoogleMapOptionsSink {
   private final GoogleMapOptions options = new GoogleMapOptions();
@@ -23,6 +25,7 @@
   private Object initialPolygons;
   private Object initialPolylines;
   private Object initialCircles;
+  private List<Map<String, ?>> initialTileOverlays;
   private Rect padding = new Rect(0, 0, 0, 0);
 
   GoogleMapController build(
@@ -44,6 +47,7 @@
     controller.setInitialPolylines(initialPolylines);
     controller.setInitialCircles(initialCircles);
     controller.setPadding(padding.top, padding.left, padding.bottom, padding.right);
+    controller.setInitialTileOverlays(initialTileOverlays);
     return controller;
   }
 
@@ -165,4 +169,9 @@
   public void setInitialCircles(Object initialCircles) {
     this.initialCircles = initialCircles;
   }
+
+  @Override
+  public void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays) {
+    this.initialTileOverlays = initialTileOverlays;
+  }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
index f6b8c3f..7db65c5 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -76,10 +76,12 @@
   private final PolygonsController polygonsController;
   private final PolylinesController polylinesController;
   private final CirclesController circlesController;
+  private final TileOverlaysController tileOverlaysController;
   private List<Object> initialMarkers;
   private List<Object> initialPolygons;
   private List<Object> initialPolylines;
   private List<Object> initialCircles;
+  private List<Map<String, ?>> initialTileOverlays;
 
   GoogleMapController(
       int id,
@@ -99,6 +101,7 @@
     this.polygonsController = new PolygonsController(methodChannel, density);
     this.polylinesController = new PolylinesController(methodChannel, density);
     this.circlesController = new CirclesController(methodChannel, density);
+    this.tileOverlaysController = new TileOverlaysController(methodChannel);
   }
 
   @Override
@@ -140,10 +143,12 @@
     polygonsController.setGoogleMap(googleMap);
     polylinesController.setGoogleMap(googleMap);
     circlesController.setGoogleMap(googleMap);
+    tileOverlaysController.setGoogleMap(googleMap);
     updateInitialMarkers();
     updateInitialPolygons();
     updateInitialPolylines();
     updateInitialCircles();
+    updateInitialTileOverlays();
   }
 
   @Override
@@ -385,6 +390,30 @@
           result.success(mapStyleResult);
           break;
         }
+      case "tileOverlays#update":
+        {
+          List<Map<String, ?>> tileOverlaysToAdd = call.argument("tileOverlaysToAdd");
+          tileOverlaysController.addTileOverlays(tileOverlaysToAdd);
+          List<Map<String, ?>> tileOverlaysToChange = call.argument("tileOverlaysToChange");
+          tileOverlaysController.changeTileOverlays(tileOverlaysToChange);
+          List<String> tileOverlaysToRemove = call.argument("tileOverlayIdsToRemove");
+          tileOverlaysController.removeTileOverlays(tileOverlaysToRemove);
+          result.success(null);
+          break;
+        }
+      case "tileOverlays#clearTileCache":
+        {
+          String tileOverlayId = call.argument("tileOverlayId");
+          tileOverlaysController.clearTileCache(tileOverlayId);
+          result.success(null);
+          break;
+        }
+      case "map#getTileOverlayInfo":
+        {
+          String tileOverlayId = call.argument("tileOverlayId");
+          result.success(tileOverlaysController.getTileOverlayInfo(tileOverlayId));
+          break;
+        }
       default:
         result.notImplemented();
     }
@@ -732,6 +761,18 @@
     circlesController.addCircles(initialCircles);
   }
 
+  @Override
+  public void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays) {
+    this.initialTileOverlays = initialTileOverlays;
+    if (googleMap != null) {
+      updateInitialTileOverlays();
+    }
+  }
+
+  private void updateInitialTileOverlays() {
+    tileOverlaysController.addTileOverlays(initialTileOverlays);
+  }
+
   @SuppressLint("MissingPermission")
   private void updateMyLocationSettings() {
     if (hasLocationPermission()) {
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
index e56adbb..bf9188f 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
@@ -10,6 +10,7 @@
 import io.flutter.plugin.common.StandardMessageCodec;
 import io.flutter.plugin.platform.PlatformView;
 import io.flutter.plugin.platform.PlatformViewFactory;
+import java.util.List;
 import java.util.Map;
 
 public class GoogleMapFactory extends PlatformViewFactory {
@@ -46,6 +47,9 @@
     if (params.containsKey("circlesToAdd")) {
       builder.setInitialCircles(params.get("circlesToAdd"));
     }
+    if (params.containsKey("tileOverlaysToAdd")) {
+      builder.setInitialTileOverlays((List<Map<String, ?>>) params.get("tileOverlaysToAdd"));
+    }
     return builder.build(id, context, binaryMessenger, lifecycleProvider);
   }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
index 9e6fa2a..03377d4 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
@@ -5,6 +5,8 @@
 package io.flutter.plugins.googlemaps;
 
 import com.google.android.gms.maps.model.LatLngBounds;
+import java.util.List;
+import java.util.Map;
 
 /** Receiver of GoogleMap configuration options. */
 interface GoogleMapOptionsSink {
@@ -51,4 +53,6 @@
   void setInitialPolylines(Object initialPolylines);
 
   void setInitialCircles(Object initialCircles);
+
+  void setInitialTileOverlays(List<Map<String, ?>> initialTileOverlays);
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java
new file mode 100644
index 0000000..1b55933
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java
@@ -0,0 +1,46 @@
+// 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.
+
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.TileOverlayOptions;
+import com.google.android.gms.maps.model.TileProvider;
+
+class TileOverlayBuilder implements TileOverlaySink {
+
+  private final TileOverlayOptions tileOverlayOptions;
+
+  TileOverlayBuilder() {
+    this.tileOverlayOptions = new TileOverlayOptions();
+  }
+
+  TileOverlayOptions build() {
+    return tileOverlayOptions;
+  }
+
+  @Override
+  public void setFadeIn(boolean fadeIn) {
+    tileOverlayOptions.fadeIn(fadeIn);
+  }
+
+  @Override
+  public void setTransparency(float transparency) {
+    tileOverlayOptions.transparency(transparency);
+  }
+
+  @Override
+  public void setZIndex(float zIndex) {
+    tileOverlayOptions.zIndex(zIndex);
+  }
+
+  @Override
+  public void setVisible(boolean visible) {
+    tileOverlayOptions.visible(visible);
+  }
+
+  @Override
+  public void setTileProvider(TileProvider tileProvider) {
+    tileOverlayOptions.tileProvider(tileProvider);
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java
new file mode 100644
index 0000000..1204bcd
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java
@@ -0,0 +1,62 @@
+// 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.
+
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.TileOverlay;
+import com.google.android.gms.maps.model.TileProvider;
+import java.util.HashMap;
+import java.util.Map;
+
+class TileOverlayController implements TileOverlaySink {
+
+  private final TileOverlay tileOverlay;
+
+  TileOverlayController(TileOverlay tileOverlay) {
+    this.tileOverlay = tileOverlay;
+  }
+
+  void remove() {
+    tileOverlay.remove();
+  }
+
+  void clearTileCache() {
+    tileOverlay.clearTileCache();
+  }
+
+  Map<String, Object> getTileOverlayInfo() {
+    Map<String, Object> tileOverlayInfo = new HashMap<>();
+    tileOverlayInfo.put("fadeIn", tileOverlay.getFadeIn());
+    tileOverlayInfo.put("transparency", tileOverlay.getTransparency());
+    tileOverlayInfo.put("id", tileOverlay.getId());
+    tileOverlayInfo.put("zIndex", tileOverlay.getZIndex());
+    tileOverlayInfo.put("visible", tileOverlay.isVisible());
+    return tileOverlayInfo;
+  }
+
+  @Override
+  public void setFadeIn(boolean fadeIn) {
+    tileOverlay.setFadeIn(fadeIn);
+  }
+
+  @Override
+  public void setTransparency(float transparency) {
+    tileOverlay.setTransparency(transparency);
+  }
+
+  @Override
+  public void setZIndex(float zIndex) {
+    tileOverlay.setZIndex(zIndex);
+  }
+
+  @Override
+  public void setVisible(boolean visible) {
+    tileOverlay.setVisible(visible);
+  }
+
+  @Override
+  public void setTileProvider(TileProvider tileProvider) {
+    // You can not change tile provider after creation
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java
new file mode 100644
index 0000000..fd611d7
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java
@@ -0,0 +1,20 @@
+// 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.
+
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.TileProvider;
+
+/** Receiver of TileOverlayOptions configuration. */
+interface TileOverlaySink {
+  void setFadeIn(boolean fadeIn);
+
+  void setTransparency(float transparency);
+
+  void setZIndex(float zIndex);
+
+  void setVisible(boolean visible);
+
+  void setTileProvider(TileProvider tileProvider);
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java
new file mode 100644
index 0000000..cd583e2
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java
@@ -0,0 +1,120 @@
+// 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.
+
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.TileOverlay;
+import com.google.android.gms.maps.model.TileOverlayOptions;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class TileOverlaysController {
+
+  private final Map<String, TileOverlayController> tileOverlayIdToController;
+  private final MethodChannel methodChannel;
+  private GoogleMap googleMap;
+
+  TileOverlaysController(MethodChannel methodChannel) {
+    this.tileOverlayIdToController = new HashMap<>();
+    this.methodChannel = methodChannel;
+  }
+
+  void setGoogleMap(GoogleMap googleMap) {
+    this.googleMap = googleMap;
+  }
+
+  void addTileOverlays(List<Map<String, ?>> tileOverlaysToAdd) {
+    if (tileOverlaysToAdd == null) {
+      return;
+    }
+    for (Map<String, ?> tileOverlayToAdd : tileOverlaysToAdd) {
+      addTileOverlay(tileOverlayToAdd);
+    }
+  }
+
+  void changeTileOverlays(List<Map<String, ?>> tileOverlaysToChange) {
+    if (tileOverlaysToChange == null) {
+      return;
+    }
+    for (Map<String, ?> tileOverlayToChange : tileOverlaysToChange) {
+      changeTileOverlay(tileOverlayToChange);
+    }
+  }
+
+  void removeTileOverlays(List<String> tileOverlayIdsToRemove) {
+    if (tileOverlayIdsToRemove == null) {
+      return;
+    }
+    for (String tileOverlayId : tileOverlayIdsToRemove) {
+      if (tileOverlayId == null) {
+        continue;
+      }
+      removeTileOverlay(tileOverlayId);
+    }
+  }
+
+  void clearTileCache(String tileOverlayId) {
+    if (tileOverlayId == null) {
+      return;
+    }
+    TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId);
+    if (tileOverlayController != null) {
+      tileOverlayController.clearTileCache();
+    }
+  }
+
+  Map<String, Object> getTileOverlayInfo(String tileOverlayId) {
+    if (tileOverlayId == null) {
+      return null;
+    }
+    TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId);
+    if (tileOverlayController == null) {
+      return null;
+    }
+    return tileOverlayController.getTileOverlayInfo();
+  }
+
+  private void addTileOverlay(Map<String, ?> tileOverlayOptions) {
+    if (tileOverlayOptions == null) {
+      return;
+    }
+    TileOverlayBuilder tileOverlayOptionsBuilder = new TileOverlayBuilder();
+    String tileOverlayId =
+        Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayOptionsBuilder);
+    TileProviderController tileProviderController =
+        new TileProviderController(methodChannel, tileOverlayId);
+    tileOverlayOptionsBuilder.setTileProvider(tileProviderController);
+    TileOverlayOptions options = tileOverlayOptionsBuilder.build();
+    TileOverlay tileOverlay = googleMap.addTileOverlay(options);
+    TileOverlayController tileOverlayController = new TileOverlayController(tileOverlay);
+    tileOverlayIdToController.put(tileOverlayId, tileOverlayController);
+  }
+
+  private void changeTileOverlay(Map<String, ?> tileOverlayOptions) {
+    if (tileOverlayOptions == null) {
+      return;
+    }
+    String tileOverlayId = getTileOverlayId(tileOverlayOptions);
+    TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId);
+    if (tileOverlayController != null) {
+      Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayController);
+    }
+  }
+
+  private void removeTileOverlay(String tileOverlayId) {
+    TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId);
+    if (tileOverlayController != null) {
+      tileOverlayController.remove();
+      tileOverlayIdToController.remove(tileOverlayId);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static String getTileOverlayId(Map<String, ?> tileOverlay) {
+    return (String) tileOverlay.get("tileOverlayId");
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java
new file mode 100644
index 0000000..f28118c
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java
@@ -0,0 +1,100 @@
+// 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.
+
+package io.flutter.plugins.googlemaps;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.android.gms.maps.model.Tile;
+import com.google.android.gms.maps.model.TileProvider;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+class TileProviderController implements TileProvider {
+
+  private static final String TAG = "TileProviderController";
+
+  private final String tileOverlayId;
+  private final MethodChannel methodChannel;
+  private final Handler handler = new Handler(Looper.getMainLooper());
+
+  TileProviderController(MethodChannel methodChannel, String tileOverlayId) {
+    this.tileOverlayId = tileOverlayId;
+    this.methodChannel = methodChannel;
+  }
+
+  @Override
+  public Tile getTile(final int x, final int y, final int zoom) {
+    Worker worker = new Worker(x, y, zoom);
+    return worker.getTile();
+  }
+
+  private final class Worker implements MethodChannel.Result {
+
+    private final CountDownLatch countDownLatch = new CountDownLatch(1);
+    private final int x;
+    private final int y;
+    private final int zoom;
+    private Map<String, ?> result;
+
+    Worker(int x, int y, int zoom) {
+      this.x = x;
+      this.y = y;
+      this.zoom = zoom;
+    }
+
+    @NonNull
+    Tile getTile() {
+      handler.post(
+          () ->
+              methodChannel.invokeMethod(
+                  "tileOverlay#getTile",
+                  Convert.tileOverlayArgumentsToJson(tileOverlayId, x, y, zoom),
+                  this));
+      try {
+        // Because `methodChannel.invokeMethod` is async, we use a `countDownLatch` make it synchronized.
+        countDownLatch.await();
+      } catch (InterruptedException e) {
+        Log.e(
+            TAG,
+            String.format("countDownLatch: can't get tile: x = %d, y= %d, zoom = %d", x, y, zoom),
+            e);
+        return TileProvider.NO_TILE;
+      }
+      try {
+        return Convert.interpretTile(result);
+      } catch (Exception e) {
+        Log.e(TAG, "Can't parse tile data", e);
+        return TileProvider.NO_TILE;
+      }
+    }
+
+    @Override
+    public void success(Object data) {
+      result = (Map<String, ?>) data;
+      countDownLatch.countDown();
+    }
+
+    @Override
+    public void error(String errorCode, String errorMessage, Object data) {
+      Log.e(
+          TAG,
+          String.format(
+              "Can't get tile: errorCode = %s, errorMessage = %s, date = %s",
+              errorCode, errorCode, data));
+      result = null;
+      countDownLatch.countDown();
+    }
+
+    @Override
+    public void notImplemented() {
+      Log.e(TAG, "Can't get tile: notImplemented");
+      result = null;
+      countDownLatch.countDown();
+    }
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart
index 2fcfec1..0f2dafb 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart
@@ -76,4 +76,11 @@
   Future<Uint8List> takeSnapshot() async {
     return await _channel.invokeMethod<Uint8List>('map#takeSnapshot');
   }
+
+  Future<Map<String, dynamic>> getTileOverlayInfo(String id) async {
+    return await _channel.invokeMapMethod<String, dynamic>(
+        'map#getTileOverlayInfo', <String, String>{
+      'tileOverlayId': id,
+    });
+  }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart
index 2a5bf80..557337f 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 import 'dart:io';
 import 'dart:typed_data';
+import 'dart:ui' as ui;
 
 import 'package:integration_test/integration_test.dart';
 import 'package:flutter/material.dart';
@@ -987,4 +988,247 @@
       // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled.
       // https://github.com/flutter/flutter/issues/57057
       skip: Platform.isAndroid);
+
+  testWidgets(
+    'set tileOverlay correctly',
+    (WidgetTester tester) async {
+      Completer<GoogleMapInspector> inspectorCompleter =
+          Completer<GoogleMapInspector>();
+      final TileOverlay tileOverlay1 = TileOverlay(
+        tileOverlayId: TileOverlayId('tile_overlay_1'),
+        tileProvider: _DebugTileProvider(),
+        zIndex: 2,
+        visible: true,
+        transparency: 0.2,
+        fadeIn: true,
+      );
+
+      final TileOverlay tileOverlay2 = TileOverlay(
+        tileOverlayId: TileOverlayId('tile_overlay_2'),
+        tileProvider: _DebugTileProvider(),
+        zIndex: 1,
+        visible: false,
+        transparency: 0.3,
+        fadeIn: false,
+      );
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: GoogleMap(
+            initialCameraPosition: _kInitialCameraPosition,
+            tileOverlays: <TileOverlay>{tileOverlay1, tileOverlay2},
+            onMapCreated: (GoogleMapController controller) {
+              final GoogleMapInspector inspector =
+                  // ignore: invalid_use_of_visible_for_testing_member
+                  GoogleMapInspector(controller.channel);
+              inspectorCompleter.complete(inspector);
+            },
+          ),
+        ),
+      );
+      await tester.pumpAndSettle(const Duration(seconds: 3));
+
+      final GoogleMapInspector inspector = await inspectorCompleter.future;
+
+      Map<String, dynamic> tileOverlayInfo1 =
+          await inspector.getTileOverlayInfo('tile_overlay_1');
+      Map<String, dynamic> tileOverlayInfo2 =
+          await inspector.getTileOverlayInfo('tile_overlay_2');
+
+      expect(tileOverlayInfo1['visible'], isTrue);
+      expect(tileOverlayInfo1['fadeIn'], isTrue);
+      expect(tileOverlayInfo1['transparency'],
+          moreOrLessEquals(0.2, epsilon: 0.001));
+      expect(tileOverlayInfo1['zIndex'], 2);
+
+      expect(tileOverlayInfo2['visible'], isFalse);
+      expect(tileOverlayInfo2['fadeIn'], isFalse);
+      expect(tileOverlayInfo2['transparency'],
+          moreOrLessEquals(0.3, epsilon: 0.001));
+      expect(tileOverlayInfo2['zIndex'], 1);
+    },
+  );
+
+  testWidgets(
+    'update tileOverlays correctly',
+    (WidgetTester tester) async {
+      Completer<GoogleMapInspector> inspectorCompleter =
+          Completer<GoogleMapInspector>();
+      final Key key = GlobalKey();
+      final TileOverlay tileOverlay1 = TileOverlay(
+        tileOverlayId: TileOverlayId('tile_overlay_1'),
+        tileProvider: _DebugTileProvider(),
+        zIndex: 2,
+        visible: true,
+        transparency: 0.2,
+        fadeIn: true,
+      );
+
+      final TileOverlay tileOverlay2 = TileOverlay(
+        tileOverlayId: TileOverlayId('tile_overlay_2'),
+        tileProvider: _DebugTileProvider(),
+        zIndex: 3,
+        visible: true,
+        transparency: 0.5,
+        fadeIn: true,
+      );
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: GoogleMap(
+            key: key,
+            initialCameraPosition: _kInitialCameraPosition,
+            tileOverlays: <TileOverlay>{tileOverlay1, tileOverlay2},
+            onMapCreated: (GoogleMapController controller) {
+              final GoogleMapInspector inspector =
+                  // ignore: invalid_use_of_visible_for_testing_member
+                  GoogleMapInspector(controller.channel);
+              inspectorCompleter.complete(inspector);
+            },
+          ),
+        ),
+      );
+
+      final GoogleMapInspector inspector = await inspectorCompleter.future;
+
+      final TileOverlay tileOverlay1New = TileOverlay(
+        tileOverlayId: TileOverlayId('tile_overlay_1'),
+        tileProvider: _DebugTileProvider(),
+        zIndex: 1,
+        visible: false,
+        transparency: 0.3,
+        fadeIn: false,
+      );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: GoogleMap(
+            key: key,
+            initialCameraPosition: _kInitialCameraPosition,
+            tileOverlays: <TileOverlay>{tileOverlay1New},
+            onMapCreated: (GoogleMapController controller) {
+              fail('update: OnMapCreated should get called only once.');
+            },
+          ),
+        ),
+      );
+
+      await tester.pumpAndSettle(const Duration(seconds: 3));
+
+      Map<String, dynamic> tileOverlayInfo1 =
+          await inspector.getTileOverlayInfo('tile_overlay_1');
+      Map<String, dynamic> tileOverlayInfo2 =
+          await inspector.getTileOverlayInfo('tile_overlay_2');
+
+      expect(tileOverlayInfo1['visible'], isFalse);
+      expect(tileOverlayInfo1['fadeIn'], isFalse);
+      expect(tileOverlayInfo1['transparency'],
+          moreOrLessEquals(0.3, epsilon: 0.001));
+      expect(tileOverlayInfo1['zIndex'], 1);
+
+      expect(tileOverlayInfo2, isNull);
+    },
+  );
+
+  testWidgets(
+    'remove tileOverlays correctly',
+    (WidgetTester tester) async {
+      Completer<GoogleMapInspector> inspectorCompleter =
+          Completer<GoogleMapInspector>();
+      final Key key = GlobalKey();
+      final TileOverlay tileOverlay1 = TileOverlay(
+        tileOverlayId: TileOverlayId('tile_overlay_1'),
+        tileProvider: _DebugTileProvider(),
+        zIndex: 2,
+        visible: true,
+        transparency: 0.2,
+        fadeIn: true,
+      );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: GoogleMap(
+            key: key,
+            initialCameraPosition: _kInitialCameraPosition,
+            tileOverlays: <TileOverlay>{tileOverlay1},
+            onMapCreated: (GoogleMapController controller) {
+              final GoogleMapInspector inspector =
+                  // ignore: invalid_use_of_visible_for_testing_member
+                  GoogleMapInspector(controller.channel);
+              inspectorCompleter.complete(inspector);
+            },
+          ),
+        ),
+      );
+
+      final GoogleMapInspector inspector = await inspectorCompleter.future;
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: GoogleMap(
+            key: key,
+            initialCameraPosition: _kInitialCameraPosition,
+            onMapCreated: (GoogleMapController controller) {
+              fail('OnMapCreated should get called only once.');
+            },
+          ),
+        ),
+      );
+
+      await tester.pumpAndSettle(const Duration(seconds: 3));
+      Map<String, dynamic> tileOverlayInfo1 =
+          await inspector.getTileOverlayInfo('tile_overlay_1');
+
+      expect(tileOverlayInfo1, isNull);
+    },
+  );
+}
+
+class _DebugTileProvider implements TileProvider {
+  _DebugTileProvider() {
+    boxPaint.isAntiAlias = true;
+    boxPaint.color = Colors.blue;
+    boxPaint.strokeWidth = 2.0;
+    boxPaint.style = PaintingStyle.stroke;
+  }
+
+  static const int width = 100;
+  static const int height = 100;
+  static final Paint boxPaint = Paint();
+  static final TextStyle textStyle = TextStyle(
+    color: Colors.red,
+    fontSize: 20,
+  );
+
+  @override
+  Future<Tile> getTile(int x, int y, int zoom) async {
+    final ui.PictureRecorder recorder = ui.PictureRecorder();
+    final Canvas canvas = Canvas(recorder);
+    final TextSpan textSpan = TextSpan(
+      text: "$x,$y",
+      style: textStyle,
+    );
+    final TextPainter textPainter = TextPainter(
+      text: textSpan,
+      textDirection: TextDirection.ltr,
+    );
+    textPainter.layout(
+      minWidth: 0.0,
+      maxWidth: width.toDouble(),
+    );
+    final Offset offset = const Offset(0, 0);
+    textPainter.paint(canvas, offset);
+    canvas.drawRect(
+        Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint);
+    final ui.Picture picture = recorder.endRecording();
+    final Uint8List byteData = await picture
+        .toImage(width, height)
+        .then((ui.Image image) =>
+            image.toByteData(format: ui.ImageByteFormat.png))
+        .then((ByteData byteData) => byteData.buffer.asUint8List());
+    return Tile(width, height, byteData);
+  }
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart
index 13763ed..b795040 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart
@@ -20,6 +20,7 @@
 import 'place_polyline.dart';
 import 'scrolling_map.dart';
 import 'snapshot.dart';
+import 'tile_overlay.dart';
 
 final List<GoogleMapExampleAppPage> _allPages = <GoogleMapExampleAppPage>[
   MapUiPage(),
@@ -36,6 +37,7 @@
   PaddingPage(),
   SnapshotPage(),
   LiteModePage(),
+  TileOverlayPage(),
 ];
 
 class MapsDemo extends StatelessWidget {
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart
new file mode 100644
index 0000000..354fa06
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart
@@ -0,0 +1,151 @@
+// 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.
+
+// ignore_for_file: public_member_api_docs
+
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+import 'page.dart';
+
+class TileOverlayPage extends GoogleMapExampleAppPage {
+  TileOverlayPage() : super(const Icon(Icons.map), 'Tile overlay');
+
+  @override
+  Widget build(BuildContext context) {
+    return const TileOverlayBody();
+  }
+}
+
+class TileOverlayBody extends StatefulWidget {
+  const TileOverlayBody();
+
+  @override
+  State<StatefulWidget> createState() => TileOverlayBodyState();
+}
+
+class TileOverlayBodyState extends State<TileOverlayBody> {
+  TileOverlayBodyState();
+
+  GoogleMapController controller;
+  TileOverlay _tileOverlay;
+
+  void _onMapCreated(GoogleMapController controller) {
+    this.controller = controller;
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  void _removeTileOverlay() {
+    setState(() {
+      _tileOverlay = null;
+    });
+  }
+
+  void _addTileOverlay() {
+    final TileOverlay tileOverlay = TileOverlay(
+      tileOverlayId: TileOverlayId('tile_overlay_1'),
+      tileProvider: _DebugTileProvider(),
+    );
+    setState(() {
+      _tileOverlay = tileOverlay;
+    });
+  }
+
+  void _clearTileCache() {
+    if (_tileOverlay != null && controller != null) {
+      controller.clearTileCache(_tileOverlay.tileOverlayId);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: <Widget>[
+        Center(
+          child: SizedBox(
+            width: 350.0,
+            height: 300.0,
+            child: GoogleMap(
+              initialCameraPosition: const CameraPosition(
+                target: LatLng(59.935460, 30.325177),
+                zoom: 7.0,
+              ),
+              tileOverlays:
+                  _tileOverlay != null ? <TileOverlay>{_tileOverlay} : null,
+              onMapCreated: _onMapCreated,
+            ),
+          ),
+        ),
+        TextButton(
+          child: const Text('Add tile overlay'),
+          onPressed: _addTileOverlay,
+        ),
+        TextButton(
+          child: const Text('Remove tile overlay'),
+          onPressed: _removeTileOverlay,
+        ),
+        TextButton(
+          child: const Text('Clear tile cache'),
+          onPressed: _clearTileCache,
+        ),
+      ],
+    );
+  }
+}
+
+class _DebugTileProvider implements TileProvider {
+  _DebugTileProvider() {
+    boxPaint.isAntiAlias = true;
+    boxPaint.color = Colors.blue;
+    boxPaint.strokeWidth = 2.0;
+    boxPaint.style = PaintingStyle.stroke;
+  }
+
+  static const int width = 100;
+  static const int height = 100;
+  static final Paint boxPaint = Paint();
+  static final TextStyle textStyle = TextStyle(
+    color: Colors.red,
+    fontSize: 20,
+  );
+
+  @override
+  Future<Tile> getTile(int x, int y, int zoom) async {
+    final ui.PictureRecorder recorder = ui.PictureRecorder();
+    final Canvas canvas = Canvas(recorder);
+    final TextSpan textSpan = TextSpan(
+      text: '$x,$y',
+      style: textStyle,
+    );
+    final TextPainter textPainter = TextPainter(
+      text: textSpan,
+      textDirection: TextDirection.ltr,
+    );
+    textPainter.layout(
+      minWidth: 0.0,
+      maxWidth: width.toDouble(),
+    );
+    final Offset offset = const Offset(0, 0);
+    textPainter.paint(canvas, offset);
+    canvas.drawRect(
+        Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint);
+    final ui.Picture picture = recorder.endRecording();
+    final Uint8List byteData = await picture
+        .toImage(width, height)
+        .then((ui.Image image) =>
+            image.toByteData(format: ui.ImageByteFormat.png))
+        .then((ByteData byteData) => byteData.buffer.asUint8List());
+    return Tile(width, height, byteData);
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h
new file mode 100644
index 0000000..f84ad7c
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h
@@ -0,0 +1,42 @@
+// 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Defines map UI options writable from Flutter.
+@protocol FLTGoogleMapTileOverlayOptionsSink
+- (void)setFadeIn:(BOOL)fadeIn;
+- (void)setTransparency:(float)transparency;
+- (void)setZIndex:(int)zIndex;
+- (void)setVisible:(BOOL)visible;
+- (void)setTileSize:(NSInteger)tileSize;
+@end
+
+@interface FLTGoogleMapTileOverlayController : NSObject <FLTGoogleMapTileOverlayOptionsSink>
+- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer mapView:(GMSMapView *)mapView;
+- (void)removeTileOverlay;
+- (void)clearTileCache;
+- (NSDictionary *)getTileOverlayInfo;
+@end
+
+@interface FLTTileProviderController : GMSTileLayer
+@property(copy, nonatomic, readonly) NSString *tileOverlayId;
+- (instancetype)init:(FlutterMethodChannel *)methodChannel tileOverlayId:(NSString *)tileOverlayId;
+@end
+
+@interface FLTTileOverlaysController : NSObject
+- (instancetype)init:(FlutterMethodChannel *)methodChannel
+             mapView:(GMSMapView *)mapView
+           registrar:(NSObject<FlutterPluginRegistrar> *)registrar;
+- (void)addTileOverlays:(NSArray *)tileOverlaysToAdd;
+- (void)changeTileOverlays:(NSArray *)tileOverlaysToChange;
+- (void)removeTileOverlayIds:(NSArray *)tileOverlayIdsToRemove;
+- (void)clearTileCache:(NSString *)tileOverlayId;
+- (nullable NSDictionary *)getTileOverlayInfo:(NSString *)tileverlayId;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m
new file mode 100644
index 0000000..7fbd7c5
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m
@@ -0,0 +1,234 @@
+// 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 "FLTGoogleMapTileOverlayController.h"
+#import "JsonConversions.h"
+
+static void InterpretTileOverlayOptions(NSDictionary* data,
+                                        id<FLTGoogleMapTileOverlayOptionsSink> sink,
+                                        NSObject<FlutterPluginRegistrar>* registrar) {
+  NSNumber* visible = data[@"visible"];
+  if (visible != nil) {
+    [sink setVisible:visible.boolValue];
+  }
+
+  NSNumber* transparency = data[@"transparency"];
+  if (transparency != nil) {
+    [sink setTransparency:transparency.floatValue];
+  }
+
+  NSNumber* zIndex = data[@"zIndex"];
+  if (zIndex != nil) {
+    [sink setZIndex:zIndex.intValue];
+  }
+
+  NSNumber* fadeIn = data[@"fadeIn"];
+  if (fadeIn != nil) {
+    [sink setFadeIn:fadeIn.boolValue];
+  }
+
+  NSNumber* tileSize = data[@"tileSize"];
+  if (tileSize != nil) {
+    [sink setTileSize:tileSize.integerValue];
+  }
+}
+
+@interface FLTGoogleMapTileOverlayController ()
+
+@property(strong, nonatomic) GMSTileLayer* layer;
+@property(weak, nonatomic) GMSMapView* mapView;
+
+@end
+
+@implementation FLTGoogleMapTileOverlayController
+
+- (instancetype)initWithTileLayer:(GMSTileLayer*)tileLayer mapView:(GMSMapView*)mapView {
+  self = [super init];
+  if (self) {
+    self.layer = tileLayer;
+    self.mapView = mapView;
+  }
+  return self;
+}
+
+- (void)removeTileOverlay {
+  self.layer.map = nil;
+}
+
+- (void)clearTileCache {
+  [self.layer clearTileCache];
+}
+
+- (NSDictionary*)getTileOverlayInfo {
+  NSMutableDictionary* info = [[NSMutableDictionary alloc] init];
+  BOOL visible = self.layer.map != nil;
+  info[@"visible"] = @(visible);
+  info[@"fadeIn"] = @(self.layer.fadeIn);
+  float transparency = 1.0 - self.layer.opacity;
+  info[@"transparency"] = @(transparency);
+  info[@"zIndex"] = @(self.layer.zIndex);
+  return info;
+}
+
+#pragma mark - FLTGoogleMapTileOverlayOptionsSink methods
+
+- (void)setFadeIn:(BOOL)fadeIn {
+  self.layer.fadeIn = fadeIn;
+}
+
+- (void)setTransparency:(float)transparency {
+  float opacity = 1.0 - transparency;
+  self.layer.opacity = opacity;
+}
+
+- (void)setVisible:(BOOL)visible {
+  self.layer.map = visible ? self.mapView : nil;
+}
+
+- (void)setZIndex:(int)zIndex {
+  self.layer.zIndex = zIndex;
+}
+
+- (void)setTileSize:(NSInteger)tileSize {
+  self.layer.tileSize = tileSize;
+}
+@end
+
+@interface FLTTileProviderController ()
+
+@property(weak, nonatomic) FlutterMethodChannel* methodChannel;
+@property(copy, nonatomic, readwrite) NSString* tileOverlayId;
+
+@end
+
+@implementation FLTTileProviderController
+
+- (instancetype)init:(FlutterMethodChannel*)methodChannel tileOverlayId:(NSString*)tileOverlayId {
+  self = [super init];
+  if (self) {
+    self.methodChannel = methodChannel;
+    self.tileOverlayId = tileOverlayId;
+  }
+  return self;
+}
+
+#pragma mark - GMSTileLayer method
+
+- (void)requestTileForX:(NSUInteger)x
+                      y:(NSUInteger)y
+                   zoom:(NSUInteger)zoom
+               receiver:(id<GMSTileReceiver>)receiver {
+  [self.methodChannel
+      invokeMethod:@"tileOverlay#getTile"
+         arguments:@{
+           @"tileOverlayId" : self.tileOverlayId,
+           @"x" : @(x),
+           @"y" : @(y),
+           @"zoom" : @(zoom)
+         }
+            result:^(id _Nullable result) {
+              UIImage* tileImage;
+              if ([result isKindOfClass:[NSDictionary class]]) {
+                FlutterStandardTypedData* typedData = (FlutterStandardTypedData*)result[@"data"];
+                if (typedData == nil) {
+                  tileImage = kGMSTileLayerNoTile;
+                } else {
+                  tileImage = [UIImage imageWithData:typedData.data];
+                }
+              } else {
+                if ([result isKindOfClass:[FlutterError class]]) {
+                  FlutterError* error = (FlutterError*)result;
+                  NSLog(@"Can't get tile: errorCode = %@, errorMessage = %@, details = %@",
+                        [error code], [error message], [error details]);
+                }
+                if ([result isKindOfClass:[FlutterMethodNotImplemented class]]) {
+                  NSLog(@"Can't get tile: notImplemented");
+                }
+                tileImage = kGMSTileLayerNoTile;
+              }
+
+              [receiver receiveTileWithX:x y:y zoom:zoom image:tileImage];
+            }];
+}
+
+@end
+
+@interface FLTTileOverlaysController ()
+
+@property(strong, nonatomic) NSMutableDictionary* tileOverlayIdToController;
+@property(weak, nonatomic) FlutterMethodChannel* methodChannel;
+@property(weak, nonatomic) NSObject<FlutterPluginRegistrar>* registrar;
+@property(weak, nonatomic) GMSMapView* mapView;
+
+@end
+
+@implementation FLTTileOverlaysController
+
+- (instancetype)init:(FlutterMethodChannel*)methodChannel
+             mapView:(GMSMapView*)mapView
+           registrar:(NSObject<FlutterPluginRegistrar>*)registrar {
+  self = [super init];
+  if (self) {
+    self.methodChannel = methodChannel;
+    self.mapView = mapView;
+    self.tileOverlayIdToController = [[NSMutableDictionary alloc] init];
+    self.registrar = registrar;
+  }
+  return self;
+}
+
+- (void)addTileOverlays:(NSArray*)tileOverlaysToAdd {
+  for (NSDictionary* tileOverlay in tileOverlaysToAdd) {
+    NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay];
+    FLTTileProviderController* tileProvider =
+        [[FLTTileProviderController alloc] init:self.methodChannel tileOverlayId:tileOverlayId];
+    FLTGoogleMapTileOverlayController* controller =
+        [[FLTGoogleMapTileOverlayController alloc] initWithTileLayer:tileProvider
+                                                             mapView:self.mapView];
+    InterpretTileOverlayOptions(tileOverlay, controller, self.registrar);
+    self.tileOverlayIdToController[tileOverlayId] = controller;
+  }
+}
+
+- (void)changeTileOverlays:(NSArray*)tileOverlaysToChange {
+  for (NSDictionary* tileOverlay in tileOverlaysToChange) {
+    NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay];
+    FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId];
+    if (!controller) {
+      continue;
+    }
+    InterpretTileOverlayOptions(tileOverlay, controller, self.registrar);
+  }
+}
+- (void)removeTileOverlayIds:(NSArray*)tileOverlayIdsToRemove {
+  for (NSString* tileOverlayId in tileOverlayIdsToRemove) {
+    FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId];
+    if (!controller) {
+      continue;
+    }
+    [controller removeTileOverlay];
+    [self.tileOverlayIdToController removeObjectForKey:tileOverlayId];
+  }
+}
+
+- (void)clearTileCache:(NSString*)tileOverlayId {
+  FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId];
+  if (!controller) {
+    return;
+  }
+  [controller clearTileCache];
+}
+
+- (nullable NSDictionary*)getTileOverlayInfo:(NSString*)tileverlayId {
+  if (self.tileOverlayIdToController[tileverlayId] == nil) {
+    return nil;
+  }
+  return [self.tileOverlayIdToController[tileverlayId] getTileOverlayInfo];
+}
+
++ (NSString*)getTileOverlayId:(NSDictionary*)tileOverlay {
+  return tileOverlay[@"tileOverlayId"];
+}
+
+@end
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m
index 321ddd3..749ce9e 100644
--- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #import "GoogleMapController.h"
+#import "FLTGoogleMapTileOverlayController.h"
 #import "JsonConversions.h"
 
 #pragma mark - Conversion of JSON-like values sent via platform channels. Forward declarations.
@@ -55,6 +56,7 @@
   FLTPolygonsController* _polygonsController;
   FLTPolylinesController* _polylinesController;
   FLTCirclesController* _circlesController;
+  FLTTileOverlaysController* _tileOverlaysController;
 }
 
 - (instancetype)initWithFrame:(CGRect)frame
@@ -94,6 +96,9 @@
     _circlesController = [[FLTCirclesController alloc] init:_channel
                                                     mapView:_mapView
                                                   registrar:registrar];
+    _tileOverlaysController = [[FLTTileOverlaysController alloc] init:_channel
+                                                              mapView:_mapView
+                                                            registrar:registrar];
     id markersToAdd = args[@"markersToAdd"];
     if ([markersToAdd isKindOfClass:[NSArray class]]) {
       [_markersController addMarkers:markersToAdd];
@@ -110,6 +115,10 @@
     if ([circlesToAdd isKindOfClass:[NSArray class]]) {
       [_circlesController addCircles:circlesToAdd];
     }
+    id tileOverlaysToAdd = args[@"tileOverlaysToAdd"];
+    if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) {
+      [_tileOverlaysController addTileOverlays:tileOverlaysToAdd];
+    }
   }
   return self;
 }
@@ -298,6 +307,24 @@
       [_circlesController removeCircleIds:circleIdsToRemove];
     }
     result(nil);
+  } else if ([call.method isEqualToString:@"tileOverlays#update"]) {
+    id tileOverlaysToAdd = call.arguments[@"tileOverlaysToAdd"];
+    if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) {
+      [_tileOverlaysController addTileOverlays:tileOverlaysToAdd];
+    }
+    id tileOverlaysToChange = call.arguments[@"tileOverlaysToChange"];
+    if ([tileOverlaysToChange isKindOfClass:[NSArray class]]) {
+      [_tileOverlaysController changeTileOverlays:tileOverlaysToChange];
+    }
+    id tileOverlayIdsToRemove = call.arguments[@"tileOverlayIdsToRemove"];
+    if ([tileOverlayIdsToRemove isKindOfClass:[NSArray class]]) {
+      [_tileOverlaysController removeTileOverlayIds:tileOverlayIdsToRemove];
+    }
+    result(nil);
+  } else if ([call.method isEqualToString:@"tileOverlays#clearTileCache"]) {
+    id rawTileOverlayId = call.arguments[@"tileOverlayId"];
+    [_tileOverlaysController clearTileCache:rawTileOverlayId];
+    result(nil);
   } else if ([call.method isEqualToString:@"map#isCompassEnabled"]) {
     NSNumber* isCompassEnabled = @(_mapView.settings.compassButton);
     result(isCompassEnabled);
@@ -341,6 +368,9 @@
     } else {
       result(@[ @(NO), error ]);
     }
+  } else if ([call.method isEqualToString:@"map#getTileOverlayInfo"]) {
+    NSString* rawTileOverlayId = call.arguments[@"tileOverlayId"];
+    result([_tileOverlaysController getTileOverlayInfo:rawTileOverlayId]);
   } else {
     result(FlutterMethodNotImplemented);
   }
diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart
index 682c901..703ba63 100644
--- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart
@@ -42,7 +42,11 @@
         PolygonId,
         Polyline,
         PolylineId,
-        ScreenCoordinate;
+        ScreenCoordinate,
+        Tile,
+        TileOverlayId,
+        TileOverlay,
+        TileProvider;
 
 part 'src/controller.dart';
 part 'src/google_map.dart';
diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart
index f47b8e5..3967179 100644
--- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart
@@ -152,6 +152,30 @@
         mapId: mapId);
   }
 
+  /// Updates tile overlays 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> _updateTileOverlays(Set<TileOverlay> newTileOverlays) {
+    return _googleMapsFlutterPlatform.updateTileOverlays(
+        newTileOverlays: newTileOverlays, mapId: mapId);
+  }
+
+  /// 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 API maintains a small
+  /// in-memory cache of tiles. If you want to cache tiles for longer, you
+  /// should implement an on-disk cache.
+  Future<void> clearTileCache(TileOverlayId tileOverlayId) async {
+    assert(tileOverlayId != null);
+    return _googleMapsFlutterPlatform.clearTileCache(tileOverlayId,
+        mapId: mapId);
+  }
+
   /// Starts an animated change of the map camera position.
   ///
   /// The returned [Future] completes after the change has been started on the
diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart
index d7f0f1a..e7f5e32 100644
--- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart
@@ -50,6 +50,7 @@
     this.polylines,
     this.circles,
     this.onCameraMoveStarted,
+    this.tileOverlays,
     this.onCameraMove,
     this.onCameraIdle,
     this.onTap,
@@ -120,6 +121,9 @@
   /// Circles to be placed on the map.
   final Set<Circle> circles;
 
+  /// Tile overlays to be placed on the map.
+  final Set<TileOverlay> tileOverlays;
+
   /// Called when the camera starts moving.
   ///
   /// This can be initiated by the following:
@@ -232,6 +236,7 @@
       'polylinesToAdd': serializePolylineSet(widget.polylines),
       'circlesToAdd': serializeCircleSet(widget.circles),
       '_webOnlyMapCreationId': _webOnlyMapCreationId,
+      'tileOverlaysToAdd': serializeTileOverlaySet(widget.tileOverlays),
     };
 
     return _googleMapsFlutterPlatform.buildView(
@@ -266,6 +271,7 @@
     _updatePolygons();
     _updatePolylines();
     _updateCircles();
+    _updateTileOverlays();
   }
 
   void _updateOptions() async {
@@ -313,6 +319,12 @@
     _circles = keyByCircleId(widget.circles);
   }
 
+  void _updateTileOverlays() async {
+    final GoogleMapController controller = await _controller.future;
+    // ignore: unawaited_futures
+    controller._updateTileOverlays(widget.tileOverlays);
+  }
+
   Future<void> onPlatformViewCreated(int id) async {
     final GoogleMapController controller = await GoogleMapController.init(
       id,
@@ -320,6 +332,7 @@
       this,
     );
     _controller.complete(controller);
+    _updateTileOverlays();
     if (widget.onMapCreated != null) {
       widget.onMapCreated(controller);
     }
diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
index ef3a06f..be5c0d4 100644
--- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
@@ -7,7 +7,7 @@
   flutter:
     sdk: flutter
   flutter_plugin_android_lifecycle: ^1.0.0
-  google_maps_flutter_platform_interface: ^1.1.0
+  google_maps_flutter_platform_interface: ^1.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart
index 9a849bd..d72ac2e 100644
--- a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart
@@ -19,6 +19,7 @@
     updatePolygons(params);
     updatePolylines(params);
     updateCircles(params);
+    updateTileOverlays(Map.castFrom<dynamic, dynamic, String, dynamic>(params));
   }
 
   MethodChannel channel;
@@ -83,6 +84,12 @@
 
   Set<Circle> circlesToChange;
 
+  Set<TileOverlayId> tileOverlayIdsToRemove;
+
+  Set<TileOverlay> tileOverlaysToAdd;
+
+  Set<TileOverlay> tileOverlaysToChange;
+
   Future<dynamic> onMethodCall(MethodCall call) {
     switch (call.method) {
       case 'map#update':
@@ -97,6 +104,10 @@
       case 'polylines#update':
         updatePolylines(call.arguments);
         return Future<void>.sync(() {});
+      case 'tileOverlays#update':
+        updateTileOverlays(
+            Map.castFrom<dynamic, dynamic, String, dynamic>(call.arguments));
+        return Future<void>.sync(() {});
       case 'circles#update':
         updateCircles(call.arguments);
         return Future<void>.sync(() {});
@@ -292,6 +303,31 @@
     circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']);
   }
 
+  void updateTileOverlays(Map<String, dynamic> updateTileOverlayUpdates) {
+    if (updateTileOverlayUpdates == null) {
+      return;
+    }
+    final List<Map<dynamic, dynamic>> tileOverlaysToAddList =
+        updateTileOverlayUpdates['tileOverlaysToAdd'] != null
+            ? List.castFrom<dynamic, Map<dynamic, dynamic>>(
+                updateTileOverlayUpdates['tileOverlaysToAdd'])
+            : null;
+    final List<String> tileOverlayIdsToRemoveList =
+        updateTileOverlayUpdates['tileOverlayIdsToRemove'] != null
+            ? List.castFrom<dynamic, String>(
+                updateTileOverlayUpdates['tileOverlayIdsToRemove'])
+            : null;
+    final List<Map<dynamic, dynamic>> tileOverlaysToChangeList =
+        updateTileOverlayUpdates['tileOverlaysToChange'] != null
+            ? List.castFrom<dynamic, Map<dynamic, dynamic>>(
+                updateTileOverlayUpdates['tileOverlaysToChange'])
+            : null;
+    tileOverlaysToAdd = _deserializeTileOverlays(tileOverlaysToAddList);
+    tileOverlayIdsToRemove =
+        _deserializeTileOverlayIds(tileOverlayIdsToRemoveList);
+    tileOverlaysToChange = _deserializeTileOverlays(tileOverlaysToChangeList);
+  }
+
   Set<CircleId> _deserializeCircleIds(List<dynamic> circleIds) {
     if (circleIds == null) {
       // TODO(iskakaushik): Remove this when collection literals makes it to stable.
@@ -329,6 +365,49 @@
     return result;
   }
 
+  Set<TileOverlayId> _deserializeTileOverlayIds(List<String> tileOverlayIds) {
+    if (tileOverlayIds == null || tileOverlayIds.isEmpty) {
+      // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+      // https://github.com/flutter/flutter/issues/28312
+      // ignore: prefer_collection_literals
+      return Set<TileOverlayId>();
+    }
+    return tileOverlayIds
+        .map((String tileOverlayId) => TileOverlayId(tileOverlayId))
+        .toSet();
+  }
+
+  Set<TileOverlay> _deserializeTileOverlays(
+      List<Map<dynamic, dynamic>> tileOverlays) {
+    if (tileOverlays == null || tileOverlays.isEmpty) {
+      // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+      // https://github.com/flutter/flutter/issues/28312
+      // ignore: prefer_collection_literals
+      return Set<TileOverlay>();
+    }
+    // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+    // https://github.com/flutter/flutter/issues/28312
+    // ignore: prefer_collection_literals
+    final Set<TileOverlay> result = Set<TileOverlay>();
+    for (Map<dynamic, dynamic> tileOverlayData in tileOverlays) {
+      final String tileOverlayId = tileOverlayData['tileOverlayId'];
+      final bool fadeIn = tileOverlayData['fadeIn'];
+      final double transparency = tileOverlayData['transparency'];
+      final int zIndex = tileOverlayData['zIndex'];
+      final bool visible = tileOverlayData['visible'];
+
+      result.add(TileOverlay(
+        tileOverlayId: TileOverlayId(tileOverlayId),
+        fadeIn: fadeIn,
+        transparency: transparency,
+        zIndex: zIndex,
+        visible: visible,
+      ));
+    }
+
+    return result;
+  }
+
   void updateOptions(Map<dynamic, dynamic> options) {
     if (options.containsKey('compassEnabled')) {
       compassEnabled = options['compassEnabled'];
diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart
new file mode 100644
index 0000000..b94d490
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart
@@ -0,0 +1,210 @@
+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<TileOverlay> _toSet({TileOverlay t1, TileOverlay t2, TileOverlay t3}) {
+  final Set<TileOverlay> res = Set<TileOverlay>.identity();
+  if (t1 != null) {
+    res.add(t1);
+  }
+  if (t2 != null) {
+    res.add(t2);
+  }
+  if (t3 != null) {
+    res.add(t3);
+  }
+  return res;
+}
+
+Widget _mapWithTileOverlays(Set<TileOverlay> tileOverlays) {
+  return Directionality(
+    textDirection: TextDirection.ltr,
+    child: GoogleMap(
+      initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)),
+      tileOverlays: tileOverlays,
+    ),
+  );
+}
+
+void main() {
+  final FakePlatformViewsController fakePlatformViewsController =
+      FakePlatformViewsController();
+
+  setUpAll(() {
+    SystemChannels.platform_views.setMockMethodCallHandler(
+        fakePlatformViewsController.fakePlatformViewsMethodHandler);
+  });
+
+  setUp(() {
+    fakePlatformViewsController.reset();
+  });
+
+  testWidgets('Initializing a tile overlay', (WidgetTester tester) async {
+    final TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.tileOverlaysToAdd.length, 1);
+
+    final TileOverlay initializedTileOverlay =
+        platformGoogleMap.tileOverlaysToAdd.first;
+    expect(initializedTileOverlay, equals(t1));
+    expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true);
+  });
+
+  testWidgets("Adding a tile overlay", (WidgetTester tester) async {
+    final TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+    final TileOverlay t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"));
+
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1)));
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1, t2: t2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.tileOverlaysToAdd.length, 1);
+
+    final TileOverlay addedTileOverlay =
+        platformGoogleMap.tileOverlaysToAdd.first;
+    expect(addedTileOverlay, equals(t2));
+    expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true);
+
+    expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true);
+  });
+
+  testWidgets("Removing a tile overlay", (WidgetTester tester) async {
+    final TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1)));
+    await tester.pumpWidget(_mapWithTileOverlays(null));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1);
+    expect(platformGoogleMap.tileOverlayIdsToRemove.first,
+        equals(t1.tileOverlayId));
+
+    expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true);
+    expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true);
+  });
+
+  testWidgets("Updating a tile overlay", (WidgetTester tester) async {
+    final TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+    final TileOverlay t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10);
+
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1)));
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.tileOverlaysToChange.length, 1);
+    expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2));
+
+    expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true);
+  });
+
+  testWidgets("Updating a tile overlay", (WidgetTester tester) async {
+    final TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+    final TileOverlay t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10);
+
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1)));
+    await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.tileOverlaysToChange.length, 1);
+
+    final TileOverlay update = platformGoogleMap.tileOverlaysToChange.first;
+    expect(update, equals(t2));
+    expect(update.zIndex, 10);
+  });
+
+  testWidgets("Multi Update", (WidgetTester tester) async {
+    TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+    TileOverlay t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"));
+    final Set<TileOverlay> prev = _toSet(t1: t1, t2: t2);
+    t1 = TileOverlay(
+        tileOverlayId: TileOverlayId("tile_overlay_1"), visible: false);
+    t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10);
+    final Set<TileOverlay> cur = _toSet(t1: t1, t2: t2);
+
+    await tester.pumpWidget(_mapWithTileOverlays(prev));
+    await tester.pumpWidget(_mapWithTileOverlays(cur));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.tileOverlaysToChange, cur);
+    expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true);
+  });
+
+  testWidgets("Multi Update", (WidgetTester tester) async {
+    TileOverlay t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"));
+    final TileOverlay t3 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3"));
+    final Set<TileOverlay> prev = _toSet(t2: t2, t3: t3);
+
+    // t1 is added, t2 is updated, t3 is removed.
+    final TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+    t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10);
+    final Set<TileOverlay> cur = _toSet(t1: t1, t2: t2);
+
+    await tester.pumpWidget(_mapWithTileOverlays(prev));
+    await tester.pumpWidget(_mapWithTileOverlays(cur));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.tileOverlaysToChange.length, 1);
+    expect(platformGoogleMap.tileOverlaysToAdd.length, 1);
+    expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1);
+
+    expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2));
+    expect(platformGoogleMap.tileOverlaysToAdd.first, equals(t1));
+    expect(platformGoogleMap.tileOverlayIdsToRemove.first,
+        equals(t3.tileOverlayId));
+  });
+
+  testWidgets("Partial Update", (WidgetTester tester) async {
+    final TileOverlay t1 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"));
+    final TileOverlay t2 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"));
+    TileOverlay t3 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3"));
+    final Set<TileOverlay> prev = _toSet(t1: t1, t2: t2, t3: t3);
+    t3 =
+        TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3"), zIndex: 10);
+    final Set<TileOverlay> cur = _toSet(t1: t1, t2: t2, t3: t3);
+
+    await tester.pumpWidget(_mapWithTileOverlays(prev));
+    await tester.pumpWidget(_mapWithTileOverlays(cur));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.tileOverlaysToChange, _toSet(t3: t3));
+    expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true);
+  });
+}