Add GoogleMap callbacks (#507)

diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java
index 6fd0284..5e24cff 100644
--- a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java
@@ -7,18 +7,13 @@
 import android.graphics.Point;
 import com.google.android.gms.maps.CameraUpdate;
 import com.google.android.gms.maps.CameraUpdateFactory;
-import com.google.android.gms.maps.GoogleMap;
-import com.google.android.gms.maps.GoogleMapOptions;
-import com.google.android.gms.maps.UiSettings;
 import com.google.android.gms.maps.model.BitmapDescriptor;
 import com.google.android.gms.maps.model.BitmapDescriptorFactory;
 import com.google.android.gms.maps.model.CameraPosition;
 import com.google.android.gms.maps.model.LatLng;
 import com.google.android.gms.maps.model.LatLngBounds;
-import com.google.android.gms.maps.model.MarkerOptions;
 import io.flutter.view.FlutterMain;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -101,62 +96,15 @@
     return ((Number) o).floatValue();
   }
 
-  static GoogleMapOptions toGoogleMapOptions(Object o) {
-    final Map<?, ?> data = toMap(o);
-    final GoogleMapOptions options = new GoogleMapOptions();
-    final Object cameraPosition = data.get("cameraPosition");
-    if (cameraPosition != null) {
-      options.camera(toCameraPosition(cameraPosition));
-    }
-    final Object cameraTargetBounds = data.get("cameraTargetBounds");
-    if (cameraTargetBounds != null) {
-      final List<?> targetData = toList(cameraTargetBounds);
-      if (targetData.get(0) != null) {
-        options.latLngBoundsForCameraTarget(toLatLngBounds(targetData.get(0)));
-      }
-    }
-    final Object compassEnabled = data.get("compassEnabled");
-    if (compassEnabled != null) {
-      options.compassEnabled(toBoolean(compassEnabled));
-    }
-    final Object mapType = data.get("mapType");
-    if (mapType != null) {
-      options.mapType(toInt(mapType));
-    }
-    final Object rotateGesturesEnabled = data.get("rotateGesturesEnabled");
-    if (rotateGesturesEnabled != null) {
-      options.rotateGesturesEnabled(toBoolean(rotateGesturesEnabled));
-    }
-    final Object scrollGesturesEnabled = data.get("scrollGesturesEnabled");
-    if (scrollGesturesEnabled != null) {
-      options.scrollGesturesEnabled(toBoolean(scrollGesturesEnabled));
-    }
-    final Object tiltGesturesEnabled = data.get("tiltGesturesEnabled");
-    if (tiltGesturesEnabled != null) {
-      options.tiltGesturesEnabled(toBoolean(tiltGesturesEnabled));
-    }
-    final Object zoomBounds = data.get("zoomBounds");
-    if (zoomBounds != null) {
-      final List<?> zoomData = toList(zoomBounds);
-      if (zoomData.get(0) != null) {
-        options.minZoomPreference(toFloat(zoomData.get(0)));
-      }
-      if (zoomData.get(1) != null) {
-        options.maxZoomPreference(toFloat(zoomData.get(1)));
-      }
-    }
-    final Object zoomGesturesEnabled = data.get("zoomGesturesEnabled");
-    if (zoomGesturesEnabled != null) {
-      options.zoomGesturesEnabled(toBoolean(zoomGesturesEnabled));
-    }
-    return options;
+  private static Float toFloatWrapper(Object o) {
+    return (o == null) ? null : toFloat(o);
   }
 
   static int toInt(Object o) {
     return ((Number) o).intValue();
   }
 
-  private static Object toJson(CameraPosition position) {
+  static Object toJson(CameraPosition position) {
     final Map<String, Object> data = new HashMap<>();
     data.put("bearing", position.bearing);
     data.put("target", toJson(position.target));
@@ -175,6 +123,9 @@
   }
 
   private static LatLngBounds toLatLngBounds(Object o) {
+    if (o == null) {
+      return null;
+    }
     final List<?> data = toList(o);
     return new LatLngBounds(toLatLng(data.get(0)), toLatLng(data.get(1)));
   }
@@ -191,25 +142,6 @@
     return (Map<?, ?>) o;
   }
 
-  static MarkerOptions toMarkerOptions(Object o) {
-    final Map<?, ?> data = toMap(o);
-    final List<?> anchor = toList(data.get("anchor"));
-    final List<?> infoWindowAnchor = toList(data.get("infoWindowAnchor"));
-    return new MarkerOptions()
-        .position(toLatLng(data.get("position")))
-        .alpha(toFloat(data.get("alpha")))
-        .anchor(toFloat(anchor.get(0)), toFloat(anchor.get(1)))
-        .draggable(toBoolean(data.get("draggable")))
-        .flat(toBoolean(data.get("flat")))
-        .icon(toBitmapDescriptor(data.get("icon")))
-        .infoWindowAnchor(toFloat(infoWindowAnchor.get(0)), toFloat(infoWindowAnchor.get(1)))
-        .rotation(toFloat(data.get("rotation")))
-        .snippet(toString(data.get("snippet")))
-        .title(toString(data.get("title")))
-        .visible(toBoolean(data.get("visible")))
-        .zIndex(toFloat(data.get("zIndex")));
-  }
-
   private static Point toPoint(Object o) {
     final List<?> data = toList(o);
     return new Point(toInt(data.get(0)), toInt(data.get(1)));
@@ -219,109 +151,110 @@
     return (String) o;
   }
 
-  /**
-   * Sets GoogleMaps user interface options extracted from the specified JSON-like value on the
-   * given GoogleMap instance.
-   *
-   * @param o the JSON-like value
-   * @param googleMap the GoogleMap instance
-   */
-  static void setMapOptions(Object o, GoogleMap googleMap) {
-    final Map<?, ?> options = toMap(o);
-    final Object cameraTargetBounds = options.get("cameraTargetBounds");
-    final UiSettings uiSettings = googleMap.getUiSettings();
-    if (cameraTargetBounds != null) {
-      final List<?> targetData = toList(cameraTargetBounds);
-      if (targetData.get(0) == null) {
-        googleMap.setLatLngBoundsForCameraTarget(null);
-      } else {
-        googleMap.setLatLngBoundsForCameraTarget(toLatLngBounds(targetData.get(0)));
-      }
-    }
-    final Object compassEnabled = options.get("compassEnabled");
-    if (compassEnabled != null) {
-      uiSettings.setCompassEnabled(toBoolean(compassEnabled));
-    }
-    final Object mapType = options.get("mapType");
-    if (mapType != null) {
-      googleMap.setMapType(toInt(mapType));
-    }
-    final Object rotateGesturesEnabled = options.get("rotateGesturesEnabled");
-    if (rotateGesturesEnabled != null) {
-      uiSettings.setRotateGesturesEnabled(toBoolean(rotateGesturesEnabled));
-    }
-    final Object scrollGesturesEnabled = options.get("scrollGesturesEnabled");
-    if (scrollGesturesEnabled != null) {
-      uiSettings.setScrollGesturesEnabled(toBoolean(scrollGesturesEnabled));
-    }
-    final Object tiltGesturesEnabled = options.get("tiltGesturesEnabled");
-    if (tiltGesturesEnabled != null) {
-      uiSettings.setTiltGesturesEnabled(toBoolean(tiltGesturesEnabled));
-    }
-    final Object zoomBounds = options.get("zoomBounds");
-    if (zoomBounds != null) {
-      final List<?> zoomData = toList(zoomBounds);
-      googleMap.resetMinMaxZoomPreference();
-      if (zoomData.get(0) != null) {
-        googleMap.setMinZoomPreference(toFloat(zoomData.get(0)));
-      }
-      if (zoomData.get(1) != null) {
-        googleMap.setMaxZoomPreference(toFloat(zoomData.get(1)));
-      }
-    }
-    final Object zoomGesturesEnabled = options.get("zoomGesturesEnabled");
-    if (zoomGesturesEnabled != null) {
-      uiSettings.setZoomGesturesEnabled(toBoolean(zoomGesturesEnabled));
-    }
-    final Object cameraPosition = options.get("cameraPosition");
+  static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) {
+    final Map<?, ?> data = toMap(o);
+    final Object cameraPosition = data.get("cameraPosition");
     if (cameraPosition != null) {
-      googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(toCameraPosition(cameraPosition)));
+      sink.setCameraPosition(toCameraPosition(cameraPosition));
+    }
+    final Object compassEnabled = data.get("compassEnabled");
+    if (compassEnabled != null) {
+      sink.setCompassEnabled(toBoolean(compassEnabled));
+    }
+    final Object latLngCameraTargetBounds = data.get("latLngCameraTargetBounds");
+    if (latLngCameraTargetBounds != null) {
+      final List<?> targetData = toList(latLngCameraTargetBounds);
+      sink.setLatLngBoundsForCameraTarget(toLatLngBounds(targetData.get(0)));
+    }
+    final Object mapType = data.get("mapType");
+    if (mapType != null) {
+      sink.setMapType(toInt(mapType));
+    }
+    final Object minMaxZoomPreference = data.get("minMaxZoomPreference");
+    if (minMaxZoomPreference != null) {
+      final List<?> zoomPreferenceData = toList(minMaxZoomPreference);
+      sink.setMinMaxZoomPreference( //
+          toFloatWrapper(zoomPreferenceData.get(0)), //
+          toFloatWrapper(zoomPreferenceData.get(1)));
+    }
+    final Object rotateGesturesEnabled = data.get("rotateGesturesEnabled");
+    if (rotateGesturesEnabled != null) {
+      sink.setRotateGesturesEnabled(toBoolean(rotateGesturesEnabled));
+    }
+    final Object scrollGesturesEnabled = data.get("scrollGesturesEnabled");
+    if (scrollGesturesEnabled != null) {
+      sink.setScrollGesturesEnabled(toBoolean(scrollGesturesEnabled));
+    }
+    final Object tiltGesturesEnabled = data.get("tiltGesturesEnabled");
+    if (tiltGesturesEnabled != null) {
+      sink.setTiltGesturesEnabled(toBoolean(tiltGesturesEnabled));
+    }
+    final Object trackCameraPosition = data.get("trackCameraPosition");
+    if (trackCameraPosition != null) {
+      sink.setTrackCameraPosition(toBoolean(trackCameraPosition));
+    }
+    final Object zoomGesturesEnabled = data.get("zoomGesturesEnabled");
+    if (zoomGesturesEnabled != null) {
+      sink.setZoomGesturesEnabled(toBoolean(zoomGesturesEnabled));
     }
   }
 
-  /**
-   * Stores GoogleMaps user interface configuration items extracted from the specified JSON-like
-   * value in the provided storage Map for cases where no getters exist in the GoogleMaps APIs.
-   *
-   * @param o the JSON-like value
-   * @param storageMap the storage Map
-   */
-  static void setMapOptionsWithNoGetters(Object o, Map<String, Object> storageMap) {
-    final Map<?, ?> options = toMap(o);
-    final Object cameraTargetBounds = options.get("cameraTargetBounds");
-    if (cameraTargetBounds != null) {
-      storageMap.put("cameraTargetBounds", cameraTargetBounds);
+  static void interpretMarkerOptions(Object o, MarkerOptionsSink sink) {
+    final Map<?, ?> data = toMap(o);
+    final Object alpha = data.get("alpha");
+    if (alpha != null) {
+      sink.setAlpha(toFloat(alpha));
     }
-    final Object zoomBounds = options.get("zoomBounds");
-    if (zoomBounds != null) {
-      storageMap.put("zoomBounds", zoomBounds);
+    final Object anchor = data.get("anchor");
+    if (anchor != null) {
+      final List<?> anchorData = toList(anchor);
+      sink.setAnchor(toFloat(anchorData.get(0)), toFloat(anchorData.get(1)));
     }
-  }
-
-  /**
-   * Extract current GoogleMaps user interface configuration items in a JSON-like value, using the
-   * specified storage Map for cases where no getters exist in the GoogleMaps APIs.
-   *
-   * @param googleMap a GoogleMap instance
-   * @param storageMap the storage Map
-   * @return a JSON-like value
-   */
-  static Object getMapOptions(GoogleMap googleMap, Map<String, Object> storageMap) {
-    final Map<String, Object> json = new HashMap<>(storageMap);
-    final UiSettings uiSettings = googleMap.getUiSettings();
-    json.put("cameraPosition", toJson(googleMap.getCameraPosition()));
-    if (!json.containsKey("cameraTargetBounds")) {
-      json.put("cameraTargetBounds", Collections.singletonList(null)); // unbounded
+    final Object consumesTapEvents = data.get("consumesTapEvents");
+    if (consumesTapEvents != null) {
+      sink.setConsumesTapEvents(toBoolean(consumesTapEvents));
     }
-    json.put("compassEnabled", uiSettings.isCompassEnabled());
-    json.put("mapType", googleMap.getMapType());
-    json.put("rotateGesturesEnabled", uiSettings.isRotateGesturesEnabled());
-    json.put("scrollGesturesEnabled", uiSettings.isScrollGesturesEnabled());
-    json.put("tiltGesturesEnabled", uiSettings.isTiltGesturesEnabled());
-    if (!json.containsKey("zoomBounds")) {
-      json.put("zoomBounds", Arrays.asList(null, null)); // unbounded
+    final Object draggable = data.get("draggable");
+    if (draggable != null) {
+      sink.setDraggable(toBoolean(draggable));
     }
-    json.put("zoomGesturesEnabled", uiSettings.isZoomGesturesEnabled());
-    return json;
+    final Object flat = data.get("flat");
+    if (flat != null) {
+      sink.setFlat(toBoolean(flat));
+    }
+    final Object icon = data.get("icon");
+    if (icon != null) {
+      sink.setIcon(toBitmapDescriptor(icon));
+    }
+    final Object infoWindowAnchor = data.get("infoWindowAnchor");
+    if (infoWindowAnchor != null) {
+      final List<?> anchorData = toList(infoWindowAnchor);
+      sink.setInfoWindowAnchor(toFloat(anchorData.get(0)), toFloat(anchorData.get(1)));
+    }
+    final Object infoWindowShown = data.get("infoWindowShown");
+    if (infoWindowShown != null) {
+      sink.setInfoWindowShown(toBoolean(infoWindowShown));
+    }
+    final Object infoWindowText = data.get("infoWindowText");
+    if (infoWindowText != null) {
+      final List<?> textData = toList(infoWindowText);
+      sink.setInfoWindowText(toString(textData.get(0)), toString(textData.get(1)));
+    }
+    final Object position = data.get("position");
+    if (position != null) {
+      sink.setPosition(toLatLng(position));
+    }
+    final Object rotation = data.get("rotation");
+    if (rotation != null) {
+      sink.setRotation(toFloat(rotation));
+    }
+    final Object visible = data.get("visible");
+    if (visible != null) {
+      sink.setVisible(toBoolean(visible));
+    }
+    final Object zIndex = data.get("zIndex");
+    if (zIndex != null) {
+      sink.setZIndex(toFloat(zIndex));
+    }
   }
 }
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapBuilder.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapBuilder.java
new file mode 100644
index 0000000..87ac319
--- /dev/null
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapBuilder.java
@@ -0,0 +1,85 @@
+// 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.googlemobilemaps;
+
+import com.google.android.gms.maps.GoogleMapOptions;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLngBounds;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class GoogleMapBuilder implements GoogleMapOptionsSink {
+  private final GoogleMapOptions options = new GoogleMapOptions();
+  private boolean trackCameraPosition = false;
+
+  GoogleMapController build(
+      AtomicInteger state,
+      PluginRegistry.Registrar registrar,
+      int width,
+      int height,
+      MethodChannel.Result result) {
+    final GoogleMapController controller =
+        new GoogleMapController(state, registrar, width, height, options, result);
+    controller.init();
+    controller.setTrackCameraPosition(trackCameraPosition);
+    return controller;
+  }
+
+  @Override
+  public void setCameraPosition(CameraPosition position) {
+    options.camera(position);
+  }
+
+  @Override
+  public void setCompassEnabled(boolean compassEnabled) {
+    options.compassEnabled(compassEnabled);
+  }
+
+  @Override
+  public void setLatLngBoundsForCameraTarget(LatLngBounds bounds) {
+    options.latLngBoundsForCameraTarget(bounds);
+  }
+
+  @Override
+  public void setMapType(int mapType) {
+    options.mapType(mapType);
+  }
+
+  @Override
+  public void setMinMaxZoomPreference(Float min, Float max) {
+    if (min != null) {
+      options.minZoomPreference(min);
+    }
+    if (max != null) {
+      options.maxZoomPreference(max);
+    }
+  }
+
+  @Override
+  public void setTrackCameraPosition(boolean trackCameraPosition) {
+    this.trackCameraPosition = trackCameraPosition;
+  }
+
+  @Override
+  public void setRotateGesturesEnabled(boolean rotateGesturesEnabled) {
+    options.rotateGesturesEnabled(rotateGesturesEnabled);
+  }
+
+  @Override
+  public void setScrollGesturesEnabled(boolean scrollGesturesEnabled) {
+    options.scrollGesturesEnabled(scrollGesturesEnabled);
+  }
+
+  @Override
+  public void setTiltGesturesEnabled(boolean tiltGesturesEnabled) {
+    options.tiltGesturesEnabled(tiltGesturesEnabled);
+  }
+
+  @Override
+  public void setZoomGesturesEnabled(boolean zoomGesturesEnabled) {
+    options.zoomGesturesEnabled(zoomGesturesEnabled);
+  }
+}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java
index 4ae0a90..3f6114d 100644
--- a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java
@@ -19,9 +19,13 @@
 import android.view.Surface;
 import android.widget.FrameLayout;
 import com.google.android.gms.maps.CameraUpdate;
+import com.google.android.gms.maps.CameraUpdateFactory;
 import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.GoogleMapOptions;
 import com.google.android.gms.maps.MapView;
 import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLngBounds;
 import com.google.android.gms.maps.model.Marker;
 import com.google.android.gms.maps.model.MarkerOptions;
 import io.flutter.plugin.common.MethodChannel;
@@ -38,9 +42,12 @@
 /** Controller of a single GoogleMaps MapView instance. */
 final class GoogleMapController
     implements Application.ActivityLifecycleCallbacks,
+        GoogleMapOptionsSink,
         OnMapReadyCallback,
         GoogleMap.SnapshotReadyCallback,
+        GoogleMap.OnMarkerClickListener,
         GoogleMap.OnCameraMoveStartedListener,
+        GoogleMap.OnCameraMoveListener,
         GoogleMap.OnCameraIdleListener {
   private final AtomicInteger activityState;
   private final FrameLayout parent;
@@ -52,10 +59,12 @@
   private final int height;
   private final MethodChannel.Result result;
   private final Timer timer;
-  private final Map<String, Marker> markers;
-  private final Map<String, Object> optionsStorage;
+  private final Map<String, MarkerController> markers;
+  private OnMarkerTappedListener onMarkerTappedListener;
+  private OnCameraMoveListener onCameraMoveListener;
   private GoogleMap googleMap;
   private Surface surface;
+  private boolean trackCameraPosition = false;
   private boolean disposed = false;
 
   GoogleMapController(
@@ -63,7 +72,7 @@
       PluginRegistry.Registrar registrar,
       int width,
       int height,
-      Object options,
+      GoogleMapOptions options,
       MethodChannel.Result result) {
     this.activityState = activityState;
     this.registrar = registrar;
@@ -75,11 +84,17 @@
     this.textureEntry = registrar.textures().createSurfaceTexture();
     this.surface = new Surface(textureEntry.surfaceTexture());
     textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
-    this.mapView = new MapView(registrar.activity(), Convert.toGoogleMapOptions(options));
+    this.mapView = new MapView(registrar.activity(), options);
     this.timer = new Timer();
     this.markers = new HashMap<>();
-    this.optionsStorage = new HashMap<>();
-    Convert.setMapOptionsWithNoGetters(options, optionsStorage);
+  }
+
+  void setOnCameraMoveListener(OnCameraMoveListener listener) {
+    this.onCameraMoveListener = listener;
+  }
+
+  void setOnMarkerTappedListener(OnMarkerTappedListener listener) {
+    this.onMarkerTappedListener = listener;
   }
 
   void init() {
@@ -140,15 +155,6 @@
     parent.addView(mapView, 0);
   }
 
-  void setMapOptions(Object json) {
-    Convert.setMapOptions(json, googleMap);
-    Convert.setMapOptionsWithNoGetters(json, optionsStorage);
-  }
-
-  Object getMapOptions() {
-    return Convert.getMapOptions(googleMap, optionsStorage);
-  }
-
   void moveCamera(CameraUpdate cameraUpdate) {
     googleMap.moveCamera(cameraUpdate);
   }
@@ -157,47 +163,26 @@
     googleMap.animateCamera(cameraUpdate);
   }
 
-  String addMarker(MarkerOptions options) {
-    final Marker marker = googleMap.addMarker(options);
-    markers.put(marker.getId(), marker);
-    return marker.getId();
+  MarkerBuilder newMarkerBuilder() {
+    return new MarkerBuilder(this);
+  }
+
+  Marker addMarker(MarkerOptions markerOptions, boolean consumesTapEvents) {
+    final Marker marker = googleMap.addMarker(markerOptions);
+    markers.put(
+        marker.getId(), new MarkerController(marker, consumesTapEvents, onMarkerTappedListener));
+    return marker;
   }
 
   void removeMarker(String markerId) {
-    final Marker marker = markers.remove(markerId);
-    if (marker != null) {
-      marker.remove();
+    final MarkerController markerController = markers.remove(markerId);
+    if (markerController != null) {
+      markerController.remove();
     }
   }
 
-  void showMarkerInfoWindow(String markerId) {
-    final Marker marker = marker(markerId);
-    marker.showInfoWindow();
-  }
-
-  void hideMarkerInfoWindow(String markerId) {
-    final Marker marker = marker(markerId);
-    marker.hideInfoWindow();
-  }
-
-  void updateMarker(String markerId, MarkerOptions options) {
-    final Marker marker = marker(markerId);
-    marker.setPosition(options.getPosition());
-    marker.setAlpha(options.getAlpha());
-    marker.setAnchor(options.getAnchorU(), options.getAnchorV());
-    marker.setDraggable(options.isDraggable());
-    marker.setFlat(options.isFlat());
-    marker.setIcon(options.getIcon());
-    marker.setInfoWindowAnchor(options.getInfoWindowAnchorU(), options.getInfoWindowAnchorV());
-    marker.setRotation(options.getRotation());
-    marker.setSnippet(options.getSnippet());
-    marker.setTitle(options.getTitle());
-    marker.setVisible(options.isVisible());
-    marker.setZIndex(options.getZIndex());
-  }
-
-  private Marker marker(String markerId) {
-    final Marker marker = markers.get(markerId);
+  MarkerController marker(String markerId) {
+    final MarkerController marker = markers.get(markerId);
     if (marker == null) {
       throw new IllegalArgumentException("Unknown marker: " + markerId);
     }
@@ -218,7 +203,9 @@
     this.googleMap = googleMap;
     result.success(id());
     googleMap.setOnCameraMoveStartedListener(this);
+    googleMap.setOnCameraMoveListener(this);
     googleMap.setOnCameraIdleListener(this);
+    googleMap.setOnMarkerClickListener(this);
     // Take snapshots until the dust settles.
     timer.schedule(newSnapshotTask(), 0);
     timer.schedule(newSnapshotTask(), 500);
@@ -229,11 +216,20 @@
 
   @Override
   public void onCameraMoveStarted(int reason) {
+    onCameraMoveListener.onCameraMoveStarted(reason);
     cancelSnapshotTimerTasks();
   }
 
   @Override
+  public void onCameraMove() {
+    if (trackCameraPosition && onCameraMoveListener != null) {
+      onCameraMoveListener.onCameraMove(googleMap.getCameraPosition());
+    }
+  }
+
+  @Override
   public void onCameraIdle() {
+    onCameraMoveListener.onCameraIdle();
     // Take snapshots until the dust settles.
     timer.schedule(newSnapshotTask(), 500);
     timer.schedule(newSnapshotTask(), 1500);
@@ -241,6 +237,12 @@
   }
 
   @Override
+  public boolean onMarkerClick(Marker marker) {
+    final MarkerController markerController = markers.get(marker.getId());
+    return (markerController != null && markerController.onTap());
+  }
+
+  @Override
   public void onSnapshotReady(Bitmap bitmap) {
     updateTexture();
   }
@@ -337,4 +339,62 @@
       googleMap.snapshot(GoogleMapController.this, bitmap);
     }
   }
+
+  // GoogleMapOptionsSink methods
+
+  @Override
+  public void setCameraPosition(CameraPosition position) {
+    googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(position));
+  }
+
+  @Override
+  public void setCompassEnabled(boolean compassEnabled) {
+    googleMap.getUiSettings().setCompassEnabled(compassEnabled);
+  }
+
+  @Override
+  public void setMapType(int mapType) {
+    googleMap.setMapType(mapType);
+  }
+
+  @Override
+  public void setLatLngBoundsForCameraTarget(LatLngBounds bounds) {
+    googleMap.setLatLngBoundsForCameraTarget(bounds);
+  }
+
+  @Override
+  public void setTrackCameraPosition(boolean trackCameraPosition) {
+    this.trackCameraPosition = trackCameraPosition;
+  }
+
+  @Override
+  public void setRotateGesturesEnabled(boolean rotateGesturesEnabled) {
+    googleMap.getUiSettings().setRotateGesturesEnabled(rotateGesturesEnabled);
+  }
+
+  @Override
+  public void setScrollGesturesEnabled(boolean scrollGesturesEnabled) {
+    googleMap.getUiSettings().setScrollGesturesEnabled(scrollGesturesEnabled);
+  }
+
+  @Override
+  public void setTiltGesturesEnabled(boolean tiltGesturesEnabled) {
+    googleMap.getUiSettings().setTiltGesturesEnabled(tiltGesturesEnabled);
+  }
+
+  @Override
+  public void setMinMaxZoomPreference(Float min, Float max) {
+    googleMap.resetMinMaxZoomPreference();
+    if (min != null) {
+      googleMap.setMinZoomPreference(min);
+    }
+    if (max != null) {
+      googleMap.setMaxZoomPreference(max);
+    }
+  }
+
+  @Override
+  public void setZoomGesturesEnabled(boolean zoomGesturesEnabled) {
+    googleMap.getUiSettings().setZoomGesturesEnabled(zoomGesturesEnabled);
+  }
 }
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapOptionsSink.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapOptionsSink.java
new file mode 100644
index 0000000..6eb764d
--- /dev/null
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapOptionsSink.java
@@ -0,0 +1,31 @@
+// 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.googlemobilemaps;
+
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLngBounds;
+
+/** Receiver of GoogleMap configuration options. */
+interface GoogleMapOptionsSink {
+  void setCameraPosition(CameraPosition position);
+
+  void setLatLngBoundsForCameraTarget(LatLngBounds bounds);
+
+  void setCompassEnabled(boolean compassEnabled);
+
+  void setMapType(int mapType);
+
+  void setTrackCameraPosition(boolean reportCameraMoveEvents);
+
+  void setRotateGesturesEnabled(boolean rotateGesturesEnabled);
+
+  void setScrollGesturesEnabled(boolean scrollGesturesEnabled);
+
+  void setTiltGesturesEnabled(boolean tiltGesturesEnabled);
+
+  void setMinMaxZoomPreference(Float min, Float max);
+
+  void setZoomGesturesEnabled(boolean zoomGesturesEnabled);
+}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java
index e581a89..542f1aa 100644
--- a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java
@@ -8,16 +8,24 @@
 import android.app.Application;
 import android.os.Bundle;
 import com.google.android.gms.maps.CameraUpdate;
-import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.Marker;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
 import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
 import io.flutter.plugin.common.MethodChannel.Result;
 import io.flutter.plugin.common.PluginRegistry.Registrar;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
+/**
+ * Plugin for controlling a set of GoogleMap views to be shown as overlays on top of the Flutter
+ * view. The overlay should be hidden during transformations or while Flutter is rendering on top of
+ * the map. A Texture drawn using GoogleMap bitmap snapshots can then be shown instead of the
+ * overlay.
+ */
 public class GoogleMobileMapsPlugin
     implements MethodCallHandler, Application.ActivityLifecycleCallbacks {
   static final int CREATED = 1;
@@ -28,18 +36,20 @@
   static final int DESTROYED = 6;
   private final Map<Long, GoogleMapController> googleMaps = new HashMap<>();
   private final Registrar registrar;
+  private final MethodChannel channel;
   private final AtomicInteger state = new AtomicInteger(0);
 
   public static void registerWith(Registrar registrar) {
-    final GoogleMobileMapsPlugin plugin = new GoogleMobileMapsPlugin(registrar);
     final MethodChannel channel =
         new MethodChannel(registrar.messenger(), "plugins.flutter.io/google_mobile_maps");
+    final GoogleMobileMapsPlugin plugin = new GoogleMobileMapsPlugin(registrar, channel);
     channel.setMethodCallHandler(plugin);
     registrar.activity().getApplication().registerActivityLifecycleCallbacks(plugin);
   }
 
-  private GoogleMobileMapsPlugin(Registrar registrar) {
+  private GoogleMobileMapsPlugin(Registrar registrar, MethodChannel channel) {
     this.registrar = registrar;
+    this.channel = channel;
   }
 
   @Override
@@ -59,23 +69,52 @@
           final int width = Convert.toInt(call.argument("width"));
           final int height = Convert.toInt(call.argument("height"));
           final Map<?, ?> options = Convert.toMap(call.argument("options"));
+          final GoogleMapBuilder builder = new GoogleMapBuilder();
+          Convert.interpretGoogleMapOptions(options, builder);
           final GoogleMapController controller =
-              new GoogleMapController(state, registrar, width, height, options, result);
+              builder.build(state, registrar, width, height, result);
           googleMaps.put(controller.id(), controller);
-          controller.init();
+          controller.setOnCameraMoveListener(
+              new OnCameraMoveListener() {
+                @Override
+                public void onCameraMoveStarted(int reason) {
+                  final Map<String, Object> arguments = new HashMap<>(2);
+                  arguments.put("map", controller.id());
+                  arguments.put("reason", reason);
+                  channel.invokeMethod("map#onCameraMoveStarted", arguments);
+                }
+
+                @Override
+                public void onCameraMove(CameraPosition position) {
+                  final Map<String, Object> arguments = new HashMap<>(2);
+                  arguments.put("map", controller.id());
+                  arguments.put("position", Convert.toJson(position));
+                  channel.invokeMethod("map#onCameraMove", arguments);
+                }
+
+                @Override
+                public void onCameraIdle() {
+                  channel.invokeMethod(
+                      "map#onCameraIdle", Collections.singletonMap("map", controller.id()));
+                }
+              });
+          controller.setOnMarkerTappedListener(
+              new OnMarkerTappedListener() {
+                @Override
+                public void onMarkerTapped(Marker marker) {
+                  final Map<String, Object> arguments = new HashMap<>(2);
+                  arguments.put("map", controller.id());
+                  arguments.put("marker", marker.getId());
+                  channel.invokeMethod("marker#onTap", arguments);
+                }
+              });
           // result.success is called from controller when the GoogleMaps instance is ready
           break;
         }
-      case "getMapOptions":
-        {
-          final GoogleMapController controller = mapsController(call);
-          result.success(controller.getMapOptions());
-          break;
-        }
       case "setMapOptions":
         {
           final GoogleMapController controller = mapsController(call);
-          controller.setMapOptions(call.argument("options"));
+          Convert.interpretGoogleMapOptions(call.argument("options"), controller);
           result.success(null);
           break;
         }
@@ -98,9 +137,9 @@
       case "addMarker":
         {
           final GoogleMapController controller = mapsController(call);
-          final MarkerOptions markerOptions =
-              Convert.toMarkerOptions(call.argument("markerOptions"));
-          final String markerId = controller.addMarker(markerOptions);
+          final MarkerBuilder markerBuilder = controller.newMarkerBuilder();
+          Convert.interpretMarkerOptions(call.argument("options"), markerBuilder);
+          final String markerId = markerBuilder.build();
           result.success(markerId);
           break;
         }
@@ -112,29 +151,12 @@
           result.success(null);
           break;
         }
-      case "marker#hideInfoWindow":
-        {
-          final GoogleMapController controller = mapsController(call);
-          final String markerId = call.argument("marker");
-          controller.hideMarkerInfoWindow(markerId);
-          result.success(null);
-          break;
-        }
-      case "marker#showInfoWindow":
-        {
-          final GoogleMapController controller = mapsController(call);
-          final String markerId = call.argument("marker");
-          controller.showMarkerInfoWindow(markerId);
-          result.success(null);
-          break;
-        }
       case "marker#update":
         {
           final GoogleMapController controller = mapsController(call);
           final String markerId = call.argument("marker");
-          final MarkerOptions markerOptions =
-              Convert.toMarkerOptions(call.argument("markerOptions"));
-          controller.updateMarker(markerId, markerOptions);
+          final MarkerController marker = controller.marker(markerId);
+          Convert.interpretMarkerOptions(call.argument("options"), marker);
           result.success(null);
           break;
         }
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerBuilder.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerBuilder.java
new file mode 100644
index 0000000..dc06eb7
--- /dev/null
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerBuilder.java
@@ -0,0 +1,96 @@
+// 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.googlemobilemaps;
+
+import com.google.android.gms.maps.model.BitmapDescriptor;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+
+class MarkerBuilder implements MarkerOptionsSink {
+  private final GoogleMapController mapController;
+  private final MarkerOptions markerOptions;
+  private boolean consumesTapEvents;
+  private boolean infoWindowShown;
+
+  MarkerBuilder(GoogleMapController mapController) {
+    this.mapController = mapController;
+    this.markerOptions = new MarkerOptions();
+  }
+
+  String build() {
+    final Marker marker = mapController.addMarker(markerOptions, consumesTapEvents);
+    if (infoWindowShown) {
+      marker.showInfoWindow();
+    }
+    return marker.getId();
+  }
+
+  @Override
+  public void setAlpha(float alpha) {
+    markerOptions.alpha(alpha);
+  }
+
+  @Override
+  public void setAnchor(float u, float v) {
+    markerOptions.anchor(u, v);
+  }
+
+  @Override
+  public void setConsumesTapEvents(boolean consumesTapEvents) {
+    this.consumesTapEvents = consumesTapEvents;
+  }
+
+  @Override
+  public void setDraggable(boolean draggable) {
+    markerOptions.draggable(draggable);
+  }
+
+  @Override
+  public void setFlat(boolean flat) {
+    markerOptions.flat(flat);
+  }
+
+  @Override
+  public void setIcon(BitmapDescriptor bitmapDescriptor) {
+    markerOptions.icon(bitmapDescriptor);
+  }
+
+  @Override
+  public void setInfoWindowAnchor(float u, float v) {
+    markerOptions.infoWindowAnchor(u, v);
+  }
+
+  @Override
+  public void setInfoWindowShown(boolean infoWindowShown) {
+    this.infoWindowShown = infoWindowShown;
+  }
+
+  @Override
+  public void setInfoWindowText(String title, String snippet) {
+    markerOptions.title(title);
+    markerOptions.snippet(snippet);
+  }
+
+  @Override
+  public void setPosition(LatLng position) {
+    markerOptions.position(position);
+  }
+
+  @Override
+  public void setRotation(float rotation) {
+    markerOptions.rotation(rotation);
+  }
+
+  @Override
+  public void setVisible(boolean visible) {
+    markerOptions.visible(visible);
+  }
+
+  @Override
+  public void setZIndex(float zIndex) {
+    markerOptions.zIndex(zIndex);
+  }
+}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerController.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerController.java
new file mode 100644
index 0000000..bd6e660
--- /dev/null
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerController.java
@@ -0,0 +1,104 @@
+// 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.googlemobilemaps;
+
+import com.google.android.gms.maps.model.BitmapDescriptor;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+
+/** Controller of a single Marker on the map. */
+class MarkerController implements MarkerOptionsSink {
+  private final Marker marker;
+  private final OnMarkerTappedListener onTappedListener;
+  private boolean consumesTapEvents;
+
+  MarkerController(
+      Marker marker, boolean consumesTapEvents, OnMarkerTappedListener onTappedListener) {
+    this.marker = marker;
+    this.consumesTapEvents = consumesTapEvents;
+    this.onTappedListener = onTappedListener;
+  }
+
+  boolean onTap() {
+    if (onTappedListener != null) {
+      onTappedListener.onMarkerTapped(marker);
+    }
+    return consumesTapEvents;
+  }
+
+  void remove() {
+    marker.remove();
+  }
+
+  @Override
+  public void setAlpha(float alpha) {
+    marker.setAlpha(alpha);
+  }
+
+  @Override
+  public void setAnchor(float u, float v) {
+    marker.setAnchor(u, v);
+  }
+
+  @Override
+  public void setConsumesTapEvents(boolean consumesTapEvents) {
+    this.consumesTapEvents = consumesTapEvents;
+  }
+
+  @Override
+  public void setDraggable(boolean draggable) {
+    marker.setDraggable(draggable);
+  }
+
+  @Override
+  public void setFlat(boolean flat) {
+    marker.setFlat(flat);
+  }
+
+  @Override
+  public void setIcon(BitmapDescriptor bitmapDescriptor) {
+    marker.setIcon(bitmapDescriptor);
+  }
+
+  @Override
+  public void setInfoWindowAnchor(float u, float v) {
+    marker.setInfoWindowAnchor(u, v);
+  }
+
+  @Override
+  public void setInfoWindowShown(boolean infoWindowShown) {
+    if (infoWindowShown) {
+      marker.showInfoWindow();
+    } else {
+      marker.hideInfoWindow();
+    }
+  }
+
+  @Override
+  public void setInfoWindowText(String title, String snippet) {
+    marker.setTitle(title);
+    marker.setSnippet(snippet);
+  }
+
+  @Override
+  public void setPosition(LatLng position) {
+    marker.setPosition(position);
+  }
+
+  @Override
+  public void setRotation(float rotation) {
+    marker.setRotation(rotation);
+  }
+
+  @Override
+  public void setVisible(boolean visible) {
+    marker.setVisible(visible);
+  }
+
+  @Override
+  public void setZIndex(float zIndex) {
+    marker.setZIndex(zIndex);
+  }
+}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerOptionsSink.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerOptionsSink.java
new file mode 100644
index 0000000..7499e40
--- /dev/null
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerOptionsSink.java
@@ -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.
+
+package io.flutter.plugins.googlemobilemaps;
+
+import com.google.android.gms.maps.model.BitmapDescriptor;
+import com.google.android.gms.maps.model.LatLng;
+
+/** Receiver of Marker configuration options. */
+interface MarkerOptionsSink {
+  void setAlpha(float alpha);
+
+  void setAnchor(float u, float v);
+
+  void setConsumesTapEvents(boolean consumesTapEvents);
+
+  void setDraggable(boolean draggable);
+
+  void setFlat(boolean flat);
+
+  void setIcon(BitmapDescriptor bitmapDescriptor);
+
+  void setInfoWindowAnchor(float u, float v);
+
+  void setInfoWindowShown(boolean shown);
+
+  void setInfoWindowText(String title, String snippet);
+
+  void setPosition(LatLng position);
+
+  void setRotation(float rotation);
+
+  void setVisible(boolean visible);
+
+  void setZIndex(float zIndex);
+}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnCameraMoveListener.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnCameraMoveListener.java
new file mode 100644
index 0000000..671c9f2
--- /dev/null
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnCameraMoveListener.java
@@ -0,0 +1,15 @@
+// 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.googlemobilemaps;
+
+import com.google.android.gms.maps.model.CameraPosition;
+
+interface OnCameraMoveListener {
+  void onCameraMoveStarted(int reason);
+
+  void onCameraMove(CameraPosition newPosition);
+
+  void onCameraIdle();
+}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnMarkerTappedListener.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnMarkerTappedListener.java
new file mode 100644
index 0000000..ce9319c
--- /dev/null
+++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnMarkerTappedListener.java
@@ -0,0 +1,11 @@
+// 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.googlemobilemaps;
+
+import com.google.android.gms.maps.model.Marker;
+
+interface OnMarkerTappedListener {
+  void onMarkerTapped(Marker marker);
+}
diff --git a/packages/google_mobile_maps/example/lib/animate_camera.dart b/packages/google_mobile_maps/example/lib/animate_camera.dart
index f867e86..abb9f1d 100644
--- a/packages/google_mobile_maps/example/lib/animate_camera.dart
+++ b/packages/google_mobile_maps/example/lib/animate_camera.dart
@@ -11,12 +11,9 @@
   AnimateCameraPage()
       : super(const Icon(Icons.map), "Camera control, animated");
 
-  final GoogleMapsOverlayController controller =
-      new GoogleMapsOverlayController.fromSize(width: 300.0, height: 200.0);
-
   @override
-  PlatformOverlayController get overlayController =>
-      controller.overlayController;
+  final GoogleMapOverlayController controller =
+      new GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0);
 
   @override
   Widget build(BuildContext context) {
@@ -24,7 +21,7 @@
       mainAxisAlignment: MainAxisAlignment.spaceEvenly,
       crossAxisAlignment: CrossAxisAlignment.stretch,
       children: <Widget>[
-        new Center(child: new GoogleMapsOverlay(controller: controller)),
+        new Center(child: new GoogleMapOverlay(controller: controller)),
         new Row(
           mainAxisAlignment: MainAxisAlignment.spaceEvenly,
           children: <Widget>[
@@ -32,7 +29,7 @@
               children: <Widget>[
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.newCameraPosition(
                         const CameraPosition(
                           bearing: 270.0,
@@ -47,7 +44,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.newLatLng(
                         const LatLng(56.1725505, 10.1850512),
                       ),
@@ -57,7 +54,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.newLatLngBounds(
                         const LatLngBounds(
                           southwest: const LatLng(-38.483935, 113.248673),
@@ -71,7 +68,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.newLatLngZoom(
                         const LatLng(37.4231613, -122.087159),
                         11.0,
@@ -82,7 +79,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.scrollBy(150.0, -225.0),
                     );
                   },
@@ -94,7 +91,7 @@
               children: <Widget>[
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.zoomBy(
                         -0.5,
                         const Offset(30.0, 20.0),
@@ -105,7 +102,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.zoomBy(-0.5),
                     );
                   },
@@ -113,7 +110,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.zoomIn(),
                     );
                   },
@@ -121,7 +118,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.zoomOut(),
                     );
                   },
@@ -129,7 +126,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.animateCamera(
+                    controller.mapController.animateCamera(
                       CameraUpdate.zoomTo(16.0),
                     );
                   },
diff --git a/packages/google_mobile_maps/example/lib/main.dart b/packages/google_mobile_maps/example/lib/main.dart
index f22e075..63c28b4 100644
--- a/packages/google_mobile_maps/example/lib/main.dart
+++ b/packages/google_mobile_maps/example/lib/main.dart
@@ -43,10 +43,10 @@
 }
 
 void main() {
-  GoogleMapsController.init();
+  GoogleMapController.init();
   final List<NavigatorObserver> observers = <NavigatorObserver>[];
   for (Page p in _allPages) {
-    observers.add(p.overlayController);
+    observers.add(p.controller.overlayController);
   }
   runApp(new MaterialApp(home: new MapsDemo(), navigatorObservers: observers));
 }
diff --git a/packages/google_mobile_maps/example/lib/map_ui.dart b/packages/google_mobile_maps/example/lib/map_ui.dart
index 6565caa..3fc73ec 100644
--- a/packages/google_mobile_maps/example/lib/map_ui.dart
+++ b/packages/google_mobile_maps/example/lib/map_ui.dart
@@ -15,8 +15,9 @@
 class MapUiPage extends Page {
   MapUiPage() : super(const Icon(Icons.map), "User interface");
 
-  final GoogleMapsOverlayController controller =
-      new GoogleMapsOverlayController.fromSize(
+  @override
+  final GoogleMapOverlayController controller =
+      new GoogleMapOverlayController.fromSize(
     width: 300.0,
     height: 200.0,
     options: const GoogleMapOptions(
@@ -24,21 +25,18 @@
         target: const LatLng(-33.852, 151.211),
         zoom: 11.0,
       ),
+      trackCameraPosition: true,
     ),
   );
 
   @override
-  PlatformOverlayController get overlayController =>
-      controller.overlayController;
-
-  @override
   Widget build(BuildContext context) {
     return new MapUiBody(controller);
   }
 }
 
 class MapUiBody extends StatefulWidget {
-  final GoogleMapsOverlayController controller;
+  final GoogleMapOverlayController controller;
 
   const MapUiBody(this.controller);
 
@@ -47,144 +45,141 @@
 }
 
 class MapUiBodyState extends State<MapUiBody> {
-  Future<GoogleMapOptions> _optionsFuture;
+  CameraPosition _position;
+  GoogleMapOptions _options;
 
   @override
   void initState() {
     super.initState();
-    _optionsFuture = widget.controller.mapsController.getMapOptions();
+    final GoogleMapController mapController = widget.controller.mapController;
+    mapController.addListener(() {
+      setState(() {
+        _options = mapController.options;
+        _position = mapController.cameraPosition;
+      });
+    });
+    _options = mapController.options;
+    _position = mapController.cameraPosition;
   }
 
-  Widget _compassToggler(GoogleMapOptions options) {
+  Widget _compassToggler() {
     return new FlatButton(
       child:
-          new Text('${options.compassEnabled ? 'disable' : 'enable'} compass'),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
-          new GoogleMapOptions(compassEnabled: !options.compassEnabled),
+          new Text('${_options.compassEnabled ? 'disable' : 'enable'} compass'),
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
+          new GoogleMapOptions(compassEnabled: !_options.compassEnabled),
         );
-        _reloadOptions();
       },
     );
   }
 
-  Widget _latLngBoundsToggler(GoogleMapOptions options) {
+  Widget _latLngBoundsToggler() {
     return new FlatButton(
       child: new Text(
-        options.cameraTargetBounds.isBounded
-            ? 'release camera target'
-            : 'bound camera target',
+        _options.latLngCameraTargetBounds.bounds == null
+            ? 'bound camera target'
+            : 'release camera target',
       ),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
           new GoogleMapOptions(
-            cameraTargetBounds: options.cameraTargetBounds.isBounded
-                ? CameraTargetBounds.unbounded
-                : const CameraTargetBounds(sydneyBounds),
+            latLngCameraTargetBounds:
+                _options.latLngCameraTargetBounds.bounds == null
+                    ? const LatLngCameraTargetBounds(sydneyBounds)
+                    : LatLngCameraTargetBounds.unbounded,
           ),
         );
-        _reloadOptions();
       },
     );
   }
 
-  Widget _zoomBoundsToggler(GoogleMapOptions options) {
+  Widget _zoomBoundsToggler() {
     return new FlatButton(
-      child: new Text(
-          options.zoomBounds.isBounded ? 'release zoom' : 'bound zoom'),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
+      child: new Text(_options.minMaxZoomPreference.minZoom == null
+          ? 'bound zoom'
+          : 'release zoom'),
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
           new GoogleMapOptions(
-            zoomBounds: options.zoomBounds.isBounded
-                ? ZoomBounds.unbounded
-                : const ZoomBounds(12.0, 16.0),
+            minMaxZoomPreference: _options.minMaxZoomPreference.minZoom == null
+                ? const MinMaxZoomPreference(12.0, 16.0)
+                : MinMaxZoomPreference.unbounded,
           ),
         );
-        _reloadOptions();
       },
     );
   }
 
-  Widget _mapTypeCycler(GoogleMapOptions options) {
+  Widget _mapTypeCycler() {
     final MapType nextType =
-        MapType.values[(options.mapType.index + 1) % MapType.values.length];
+        MapType.values[(_options.mapType.index + 1) % MapType.values.length];
     return new FlatButton(
       child: new Text('change map type to $nextType'),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
           new GoogleMapOptions(mapType: nextType),
         );
-        _reloadOptions();
       },
     );
   }
 
-  Widget _rotateToggler(GoogleMapOptions options) {
+  Widget _rotateToggler() {
     return new FlatButton(
       child: new Text(
-          '${options.rotateGesturesEnabled ? 'disable' : 'enable'} rotate'),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
+          '${_options.rotateGesturesEnabled ? 'disable' : 'enable'} rotate'),
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
           new GoogleMapOptions(
-            rotateGesturesEnabled: !options.rotateGesturesEnabled,
+            rotateGesturesEnabled: !_options.rotateGesturesEnabled,
           ),
         );
-        _reloadOptions();
       },
     );
   }
 
-  Widget _scrollToggler(GoogleMapOptions options) {
+  Widget _scrollToggler() {
     return new FlatButton(
       child: new Text(
-          '${options.scrollGesturesEnabled ? 'disable' : 'enable'} scroll'),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
+          '${_options.scrollGesturesEnabled ? 'disable' : 'enable'} scroll'),
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
           new GoogleMapOptions(
-            scrollGesturesEnabled: !options.scrollGesturesEnabled,
+            scrollGesturesEnabled: !_options.scrollGesturesEnabled,
           ),
         );
-        _reloadOptions();
       },
     );
   }
 
-  Widget _tiltToggler(GoogleMapOptions options) {
+  Widget _tiltToggler() {
     return new FlatButton(
       child: new Text(
-          '${options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
+          '${_options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'),
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
           new GoogleMapOptions(
-            tiltGesturesEnabled: !options.tiltGesturesEnabled,
+            tiltGesturesEnabled: !_options.tiltGesturesEnabled,
           ),
         );
-        _reloadOptions();
       },
     );
   }
 
-  Widget _zoomToggler(GoogleMapOptions options) {
+  Widget _zoomToggler() {
     return new FlatButton(
       child: new Text(
-          '${options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'),
-      onPressed: () async {
-        await widget.controller.mapsController.setMapOptions(
+          '${_options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'),
+      onPressed: () {
+        widget.controller.mapController.updateMapOptions(
           new GoogleMapOptions(
-            zoomGesturesEnabled: !options.zoomGesturesEnabled,
+            zoomGesturesEnabled: !_options.zoomGesturesEnabled,
           ),
         );
-        _reloadOptions();
       },
     );
   }
 
-  void _reloadOptions() {
-    setState(() {
-      _optionsFuture = widget.controller.mapsController.getMapOptions();
-    });
-  }
-
   @override
   Widget build(BuildContext context) {
     return new Column(
@@ -194,35 +189,26 @@
         new Padding(
           padding: const EdgeInsets.all(10.0),
           child: new Center(
-            child: new GoogleMapsOverlay(controller: widget.controller),
+            child: new GoogleMapOverlay(controller: widget.controller),
           ),
         ),
-        new FutureBuilder<GoogleMapOptions>(
-          future: _optionsFuture,
-          builder: (_, AsyncSnapshot<GoogleMapOptions> snapshot) {
-            if (!snapshot.hasData) {
-              return const Text('Loading settings');
-            } else {
-              final GoogleMapOptions options = snapshot.data;
-              return new Column(
-                children: <Widget>[
-                  new Text('camera bearing: ${options.cameraPosition.bearing}'),
-                  new Text(
-                      'camera target: ${options.cameraPosition.target.latitude},${options.cameraPosition.target.longitude}'),
-                  new Text('camera zoom: ${options.cameraPosition.zoom}'),
-                  new Text('camera tilt: ${options.cameraPosition.tilt}'),
-                  _compassToggler(options),
-                  _latLngBoundsToggler(options),
-                  _mapTypeCycler(options),
-                  _zoomBoundsToggler(options),
-                  _rotateToggler(options),
-                  _scrollToggler(options),
-                  _tiltToggler(options),
-                  _zoomToggler(options),
-                ],
-              );
-            }
-          },
+        new Column(
+          children: <Widget>[
+            new Text('camera bearing: ${_position.bearing}'),
+            new Text(
+                'camera target: ${_position.target.latitude.toStringAsFixed(4)},'
+                '${_position.target.longitude.toStringAsFixed(4)}'),
+            new Text('camera zoom: ${_position.zoom}'),
+            new Text('camera tilt: ${_position.tilt}'),
+            _compassToggler(),
+            _latLngBoundsToggler(),
+            _mapTypeCycler(),
+            _zoomBoundsToggler(),
+            _rotateToggler(),
+            _scrollToggler(),
+            _tiltToggler(),
+            _zoomToggler(),
+          ],
         ),
       ],
     );
diff --git a/packages/google_mobile_maps/example/lib/move_camera.dart b/packages/google_mobile_maps/example/lib/move_camera.dart
index 4a396f9..209d2bb 100644
--- a/packages/google_mobile_maps/example/lib/move_camera.dart
+++ b/packages/google_mobile_maps/example/lib/move_camera.dart
@@ -10,12 +10,9 @@
 class MoveCameraPage extends Page {
   MoveCameraPage() : super(const Icon(Icons.map), "Camera control");
 
-  final GoogleMapsOverlayController controller =
-      new GoogleMapsOverlayController.fromSize(width: 300.0, height: 200.0);
-
   @override
-  PlatformOverlayController get overlayController =>
-      controller.overlayController;
+  final GoogleMapOverlayController controller =
+      new GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0);
 
   @override
   Widget build(BuildContext context) {
@@ -23,7 +20,7 @@
       mainAxisAlignment: MainAxisAlignment.spaceEvenly,
       crossAxisAlignment: CrossAxisAlignment.stretch,
       children: <Widget>[
-        new Center(child: new GoogleMapsOverlay(controller: controller)),
+        new Center(child: new GoogleMapOverlay(controller: controller)),
         new Row(
           mainAxisAlignment: MainAxisAlignment.spaceEvenly,
           children: <Widget>[
@@ -31,7 +28,7 @@
               children: <Widget>[
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.newCameraPosition(
                         const CameraPosition(
                           bearing: 270.0,
@@ -46,7 +43,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.newLatLng(
                         const LatLng(56.1725505, 10.1850512),
                       ),
@@ -56,7 +53,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.newLatLngBounds(
                         const LatLngBounds(
                           southwest: const LatLng(-38.483935, 113.248673),
@@ -70,7 +67,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.newLatLngZoom(
                         const LatLng(37.4231613, -122.087159),
                         11.0,
@@ -81,7 +78,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.scrollBy(150.0, -225.0),
                     );
                   },
@@ -93,7 +90,7 @@
               children: <Widget>[
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.zoomBy(
                         -0.5,
                         const Offset(30.0, 20.0),
@@ -104,7 +101,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.zoomBy(-0.5),
                     );
                   },
@@ -112,7 +109,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.zoomIn(),
                     );
                   },
@@ -120,7 +117,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.zoomOut(),
                     );
                   },
@@ -128,7 +125,7 @@
                 ),
                 new FlatButton(
                   onPressed: () {
-                    controller.mapsController.moveCamera(
+                    controller.mapController.moveCamera(
                       CameraUpdate.zoomTo(16.0),
                     );
                   },
diff --git a/packages/google_mobile_maps/example/lib/page.dart b/packages/google_mobile_maps/example/lib/page.dart
index d47125d..0971918 100644
--- a/packages/google_mobile_maps/example/lib/page.dart
+++ b/packages/google_mobile_maps/example/lib/page.dart
@@ -11,5 +11,5 @@
   final Widget leading;
   final String title;
 
-  PlatformOverlayController get overlayController;
+  GoogleMapOverlayController get controller;
 }
diff --git a/packages/google_mobile_maps/example/lib/place_marker.dart b/packages/google_mobile_maps/example/lib/place_marker.dart
index 2bb5341..8551c3e 100644
--- a/packages/google_mobile_maps/example/lib/place_marker.dart
+++ b/packages/google_mobile_maps/example/lib/place_marker.dart
@@ -12,16 +12,18 @@
 class PlaceMarkerPage extends Page {
   PlaceMarkerPage() : super(const Icon(Icons.place), "Place marker");
 
-  final GoogleMapsOverlayController controller =
-      new GoogleMapsOverlayController.fromSize(width: 300.0, height: 200.0)
-        ..mapsController.moveCamera(CameraUpdate.newLatLngZoom(
-          const LatLng(-33.852, 151.211),
-          11.0,
-        ));
-
   @override
-  PlatformOverlayController get overlayController =>
-      controller.overlayController;
+  final GoogleMapOverlayController controller =
+      new GoogleMapOverlayController.fromSize(
+    width: 300.0,
+    height: 200.0,
+    options: const GoogleMapOptions(
+      cameraPosition: const CameraPosition(
+        target: const LatLng(-33.852, 151.211),
+        zoom: 11.0,
+      ),
+    ),
+  );
 
   @override
   Widget build(BuildContext context) {
@@ -30,7 +32,7 @@
 }
 
 class PlaceMarkerBody extends StatefulWidget {
-  final GoogleMapsOverlayController controller;
+  final GoogleMapOverlayController controller;
 
   const PlaceMarkerBody(this.controller);
 
@@ -41,151 +43,128 @@
 class PlaceMarkerBodyState extends State<PlaceMarkerBody> {
   static const LatLng center = const LatLng(-33.86711, 151.1947171);
 
-  final List<Future<Marker>> markers = <Future<Marker>>[];
-  int _nextAlpha = 1;
-  int _nextHue = 1;
-  int _nextPosition = 1;
-  int _nextRotation = 1;
-  int _nextAnchor = 1;
-  int _nextInfoWindowAnchor = 7;
-  int _nextTitle = 1;
-  int _nextZIndex = 1;
+  int _markerCount = 0;
+  Marker _selectedMarker;
+
+  @override
+  void initState() {
+    super.initState();
+    widget.controller.mapController.onMarkerTapped.add((Marker marker) {
+      if (_selectedMarker != null) {
+        _selectedMarker
+            .update(const MarkerOptions(icon: BitmapDescriptor.defaultMarker));
+      }
+      setState(() {
+        _selectedMarker = marker;
+      });
+      _selectedMarker.update(new MarkerOptions(
+          icon: BitmapDescriptor
+              .defaultMarkerWithHue(BitmapDescriptor.hueGreen)));
+    });
+  }
 
   void _add() {
+    widget.controller.mapController.addMarker(new MarkerOptions(
+      position: new LatLng(
+        center.latitude + sin(_markerCount * pi / 6.0) / 20.0,
+        center.longitude + cos(_markerCount * pi / 6.0) / 20.0,
+      ),
+      infoWindowText: new InfoWindowText('Marker #${_markerCount + 1}', '*'),
+    ));
     setState(() {
-      markers.add(
-        widget.controller.mapsController.addMarker(
-          new MarkerOptions(
-            position: new LatLng(
-              center.latitude + sin(markers.length * pi / 6.0) / 20.0,
-              center.longitude + cos(markers.length * pi / 6.0) / 20.0,
-            ),
-            zIndex: markers.length.toDouble(),
-            icon: BitmapDescriptor.defaultMarkerWithHue(30.0 * markers.length),
-          ),
-        ),
-      );
+      _markerCount += 1;
     });
   }
 
-  Future<void> _remove() async {
-    final Marker marker = await markers.last;
+  void _remove() {
+    _selectedMarker.remove();
     setState(() {
-      markers.removeLast();
+      _selectedMarker = null;
+      _markerCount -= 1;
     });
-    await marker.remove();
   }
 
-  Future<void> _showInfo() async {
-    final Marker marker = await markers.last;
-    await marker.showInfoWindow();
-  }
-
-  Future<void> _hideInfo() async {
-    final Marker marker = await markers.last;
-    await marker.hideInfoWindow();
-  }
-
-  Future<void> _changePosition() async {
-    final Marker marker = await markers.last;
-    final LatLng position = new LatLng(
-      center.latitude + sin(_nextPosition * pi / 6.0) / 20.0,
-      center.longitude + cos(_nextPosition * pi / 6.0) / 20.0,
-    );
-    setState(() {
-      _nextPosition = (_nextPosition + 1) % 12;
-    });
-    await marker.update(marker.options.copyWith(position: position));
-  }
-
-  Future<void> _changeAnchor() async {
-    final Marker marker = await markers.last;
+  void _changePosition() {
+    final LatLng current = _selectedMarker.options.position;
     final Offset offset = new Offset(
-      (sin(_nextAnchor * pi / 6.0) + 1.0) / 2.0,
-      (cos(_nextAnchor * pi / 6.0) + 1.0) / 2.0,
+      center.latitude - current.latitude,
+      center.longitude - current.longitude,
     );
-    setState(() {
-      _nextAnchor = (_nextAnchor + 1) % 12;
-    });
-    await marker.update(marker.options.copyWith(anchor: offset));
+    _selectedMarker.update(
+      new MarkerOptions(
+        position: new LatLng(
+          center.latitude + offset.dy,
+          center.longitude + offset.dx,
+        ),
+      ),
+    );
+  }
+
+  void _changeAnchor() {
+    final Offset currentAnchor = _selectedMarker.options.anchor;
+    final Offset newAnchor =
+        new Offset(1.0 - currentAnchor.dy, currentAnchor.dx);
+    _selectedMarker.update(new MarkerOptions(anchor: newAnchor));
   }
 
   Future<void> _changeInfoAnchor() async {
-    final Marker marker = await markers.last;
-    final Offset offset = new Offset(
-      (sin(_nextInfoWindowAnchor * pi / 6.0) + 1.0) / 2.0,
-      (cos(_nextInfoWindowAnchor * pi / 6.0) + 1.0) / 2.0,
-    );
-    setState(() {
-      _nextInfoWindowAnchor = (_nextInfoWindowAnchor + 1) % 12;
-    });
-    await marker.update(marker.options.copyWith(infoWindowAnchor: offset));
+    final Offset currentAnchor = _selectedMarker.options.infoWindowAnchor;
+    final Offset newAnchor =
+        new Offset(1.0 - currentAnchor.dy, currentAnchor.dx);
+    _selectedMarker.update(new MarkerOptions(infoWindowAnchor: newAnchor));
   }
 
   Future<void> _toggleDraggable() async {
-    final Marker marker = await markers.last;
-    await marker.update(
-      marker.options.copyWith(draggable: !marker.options.draggable),
-    );
+    _selectedMarker.update(
+        new MarkerOptions(draggable: !_selectedMarker.options.draggable));
   }
 
   Future<void> _toggleFlat() async {
-    final Marker marker = await markers.last;
-    await marker.update(
-      marker.options.copyWith(flat: !marker.options.flat),
-    );
+    _selectedMarker
+        .update(new MarkerOptions(flat: !_selectedMarker.options.flat));
   }
 
   Future<void> _changeInfo() async {
-    final Marker marker = await markers.last;
-    final String title = _nextTitle == 0 ? null : 'Title $_nextTitle';
-    final String snippet = _nextTitle % 2 == 0 ? null : 'Snippet $_nextTitle';
-    setState(() {
-      _nextTitle = (_nextTitle + 1) % 12;
-    });
-    await marker.update(
-      marker.options.copyWith(title: title, snippet: snippet),
-    );
+    final InfoWindowText currentInfo = _selectedMarker.options.infoWindowText;
+    _selectedMarker.update(new MarkerOptions(
+      infoWindowText: new InfoWindowText(
+        currentInfo.title,
+        currentInfo.snippet + '*',
+      ),
+    ));
   }
 
-  Future<void> _changeIcon() async {
-    final Marker marker = await markers.last;
-    final BitmapDescriptor icon = BitmapDescriptor.defaultMarkerWithHue(
-      _nextHue * 30.0,
+  Future<void> _toggleInfoShown() async {
+    _selectedMarker.update(
+      new MarkerOptions(
+          infoWindowShown: !_selectedMarker.options.infoWindowShown),
     );
-    setState(() {
-      _nextHue = (_nextHue + 1) % 12;
-    });
-    await marker.update(marker.options.copyWith(icon: icon));
   }
 
   Future<void> _changeAlpha() async {
-    final Marker marker = await markers.last;
-    final double alpha = 1.0 - _nextAlpha / 12.0;
-    setState(() {
-      _nextAlpha = (_nextAlpha + 1) % 12;
-    });
-    await marker.update(marker.options.copyWith(alpha: alpha));
-  }
-
-  Future<void> _changeRotation() async {
-    final Marker marker = await markers.last;
-    final double rotation = _nextRotation * 30.0;
-    setState(() {
-      _nextRotation = (_nextRotation + 1) % 12;
-    });
-    await marker.update(
-      marker.options.copyWith(rotation: rotation),
+    final double current = _selectedMarker.options.alpha;
+    _selectedMarker.update(
+      new MarkerOptions(alpha: current < 0.1 ? 1.0 : current * 0.75),
     );
   }
 
+  Future<void> _changeRotation() async {
+    final double current = _selectedMarker.options.rotation;
+    _selectedMarker.update(
+      new MarkerOptions(rotation: current == 330.0 ? 0.0 : current + 30.0),
+    );
+  }
+
+  Future<void> _toggleVisible() async {
+    _selectedMarker
+        .update(new MarkerOptions(visible: !_selectedMarker.options.visible));
+  }
+
   Future<void> _changeZIndex() async {
-    final Marker marker = await markers.last;
-    final double zIndex = _nextZIndex.toDouble();
-    setState(() {
-      _nextZIndex = (_nextZIndex + 1) % 12;
-    });
-    await marker.update(marker.options.copyWith(zIndex: zIndex));
+    final double current = _selectedMarker.options.zIndex;
+    _selectedMarker.update(
+      new MarkerOptions(zIndex: current == 12.0 ? 0.0 : current + 1.0),
+    );
   }
 
   @override
@@ -194,7 +173,7 @@
       mainAxisAlignment: MainAxisAlignment.spaceEvenly,
       crossAxisAlignment: CrossAxisAlignment.stretch,
       children: <Widget>[
-        new Center(child: new GoogleMapsOverlay(controller: widget.controller)),
+        new Center(child: new GoogleMapOverlay(controller: widget.controller)),
         new Row(
           mainAxisAlignment: MainAxisAlignment.spaceEvenly,
           children: <Widget>[
@@ -204,63 +183,68 @@
                   children: <Widget>[
                     new FlatButton(
                       child: const Text('add'),
-                      onPressed: (markers.length == 12) ? null : _add,
+                      onPressed: (_markerCount == 12) ? null : _add,
                     ),
                     new FlatButton(
                       child: const Text('remove'),
-                      onPressed: (markers.isEmpty) ? null : _remove,
+                      onPressed: (_selectedMarker == null) ? null : _remove,
                     ),
                     new FlatButton(
-                      child: const Text('show info'),
-                      onPressed: (markers.isEmpty) ? null : _showInfo,
+                      child: const Text('change info'),
+                      onPressed: (_selectedMarker == null) ? null : _changeInfo,
                     ),
                     new FlatButton(
-                      child: const Text('hide info'),
-                      onPressed: (markers.isEmpty) ? null : _hideInfo,
+                      child: const Text('change info anchor'),
+                      onPressed:
+                          (_selectedMarker == null) ? null : _changeInfoAnchor,
+                    ),
+                    new FlatButton(
+                      child: const Text('toggle info shown'),
+                      onPressed:
+                          (_selectedMarker == null) ? null : _toggleInfoShown,
                     ),
                   ],
                 ),
                 new Column(
                   children: <Widget>[
                     new FlatButton(
-                      child: const Text('change position'),
-                      onPressed: (markers.isEmpty) ? null : _changePosition,
+                      child: const Text('change alpha'),
+                      onPressed:
+                          (_selectedMarker == null) ? null : _changeAlpha,
                     ),
                     new FlatButton(
                       child: const Text('change anchor'),
-                      onPressed: (markers.isEmpty) ? null : _changeAnchor,
-                    ),
-                    new FlatButton(
-                      child: const Text('change info anchor'),
-                      onPressed: (markers.isEmpty) ? null : _changeInfoAnchor,
+                      onPressed:
+                          (_selectedMarker == null) ? null : _changeAnchor,
                     ),
                     new FlatButton(
                       child: const Text('toggle draggable'),
-                      onPressed: (markers.isEmpty) ? null : _toggleDraggable,
+                      onPressed:
+                          (_selectedMarker == null) ? null : _toggleDraggable,
                     ),
                     new FlatButton(
                       child: const Text('toggle flat'),
-                      onPressed: (markers.isEmpty) ? null : _toggleFlat,
+                      onPressed: (_selectedMarker == null) ? null : _toggleFlat,
                     ),
                     new FlatButton(
-                      child: const Text('change info'),
-                      onPressed: (markers.isEmpty) ? null : _changeInfo,
-                    ),
-                    new FlatButton(
-                      child: const Text('change color'),
-                      onPressed: (markers.isEmpty) ? null : _changeIcon,
-                    ),
-                    new FlatButton(
-                      child: const Text('change alpha'),
-                      onPressed: (markers.isEmpty) ? null : _changeAlpha,
+                      child: const Text('change position'),
+                      onPressed:
+                          (_selectedMarker == null) ? null : _changePosition,
                     ),
                     new FlatButton(
                       child: const Text('change rotation'),
-                      onPressed: (markers.isEmpty) ? null : _changeRotation,
+                      onPressed:
+                          (_selectedMarker == null) ? null : _changeRotation,
+                    ),
+                    new FlatButton(
+                      child: const Text('toggle visible'),
+                      onPressed:
+                          (_selectedMarker == null) ? null : _toggleVisible,
                     ),
                     new FlatButton(
                       child: const Text('change zIndex'),
-                      onPressed: (markers.isEmpty) ? null : _changeZIndex,
+                      onPressed:
+                          (_selectedMarker == null) ? null : _changeZIndex,
                     ),
                   ],
                 ),
diff --git a/packages/google_mobile_maps/lib/google_mobile_maps.dart b/packages/google_mobile_maps/lib/google_mobile_maps.dart
index 2b12899..a58533e 100644
--- a/packages/google_mobile_maps/lib/google_mobile_maps.dart
+++ b/packages/google_mobile_maps/lib/google_mobile_maps.dart
@@ -15,6 +15,7 @@
 export 'dart:async';
 
 part 'src/bitmap.dart';
+part 'src/callbacks.dart';
 part 'src/camera.dart';
 part 'src/controller.dart';
 part 'src/ui.dart';
diff --git a/packages/google_mobile_maps/lib/src/callbacks.dart b/packages/google_mobile_maps/lib/src/callbacks.dart
new file mode 100644
index 0000000..82a9abe
--- /dev/null
+++ b/packages/google_mobile_maps/lib/src/callbacks.dart
@@ -0,0 +1,44 @@
+// 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_mobile_maps;
+
+/// Callback function taking a single argument.
+typedef void ArgumentCallback<T>(T argument);
+
+/// Mutable collection of [ArgumentCallback] instances, itself an [ArgumentCallback].
+///
+/// Additions and removals happening during a single [call] invocation do not
+/// change who gets a callback until the next such invocation.
+class ArgumentCallbacks<T> {
+  final List<ArgumentCallback<T>> _callbacks = <ArgumentCallback<T>>[];
+  VoidCallback _onEmptyChanged;
+
+  void call(T argument) {
+    final int length = _callbacks.length;
+    if (length == 1) {
+      _callbacks[0].call(argument);
+    } else if (0 < length) {
+      for (ArgumentCallback<T> callback
+          in new List<ArgumentCallback<T>>.from(_callbacks)) {
+        callback(argument);
+      }
+    }
+  }
+
+  void add(ArgumentCallback<T> callback) {
+    _callbacks.add(callback);
+    if (_onEmptyChanged != null && _callbacks.length == 1) _onEmptyChanged();
+  }
+
+  void remove(ArgumentCallback<T> callback) {
+    final bool removed = _callbacks.remove(callback);
+    if (_onEmptyChanged != null && removed && _callbacks.isEmpty)
+      _onEmptyChanged();
+  }
+
+  bool get isEmpty => _callbacks.isEmpty;
+
+  bool get isNotEmpty => _callbacks.isNotEmpty;
+}
diff --git a/packages/google_mobile_maps/lib/src/controller.dart b/packages/google_mobile_maps/lib/src/controller.dart
index 779d65e..a291840 100644
--- a/packages/google_mobile_maps/lib/src/controller.dart
+++ b/packages/google_mobile_maps/lib/src/controller.dart
@@ -7,35 +7,107 @@
 final MethodChannel _channel =
     const MethodChannel('plugins.flutter.io/google_mobile_maps');
 
-/// Controller for a single GoogleMaps instance.
+/// Controller for a single GoogleMap instance.
 ///
-/// Used for programmatically controlling a platform-specific
-/// GoogleMaps view, once created.
-class GoogleMapsController {
+/// Used for programmatically controlling a platform-specific GoogleMap view.
+///
+/// Change listeners are notified upon changes to any of
+///
+/// * the [options] property,
+/// * the collection of [Marker]s added to this map
+/// * the [cameraPosition] property,
+///
+/// Listeners are notified when changes have been applied on the platform side.
+///
+/// Marker tap events can be received by adding callbacks to [onMarkerTapped].
+class GoogleMapController extends ChangeNotifier {
+  GoogleMapController._({
+    this.id,
+    GoogleMapOptions options,
+  }) : _options = options {
+    id.then((int id) {
+      _controllers[id] = this;
+    });
+    if (options.trackCameraPosition) {
+      _cameraPosition = options.cameraPosition;
+    }
+  }
+
   /// An ID identifying the GoogleMaps instance, once created.
   final Future<int> id;
 
-  GoogleMapsController(this.id);
+  final ArgumentCallbacks<Marker> onMarkerTapped =
+      new ArgumentCallbacks<Marker>();
+
+  /// The configuration options most recently applied via controller
+  /// initialization or [updateMapOptions].
+  GoogleMapOptions get options => _options;
+  GoogleMapOptions _options;
+
+  Set<Marker> get markers => new Set<Marker>.from(_markers.values);
+  final Map<String, Marker> _markers = <String, Marker>{};
+
+  bool get isCameraMoving => _isCameraMoving;
+  bool _isCameraMoving = false;
+
+  /// Returns the most recent camera position reported by the platform side.
+  /// Will be null, if camera position tracking is not enabled via
+  /// [GoogleMapOptions].
+  CameraPosition get cameraPosition => _cameraPosition;
+  CameraPosition _cameraPosition;
+
+  static Map<int, GoogleMapController> _controllers =
+      <int, GoogleMapController>{};
 
   static Future<void> init() async {
     await _channel.invokeMethod('init');
+    _channel.setMethodCallHandler((MethodCall call) {
+      final int mapId = call.arguments['map'];
+      final GoogleMapController controller = _controllers[mapId];
+      if (controller != null) {
+        controller._handleMethodCall(call);
+      }
+    });
   }
 
-  Future<void> setMapOptions(GoogleMapOptions options) async {
+  void _handleMethodCall(MethodCall call) {
+    switch (call.method) {
+      case "marker#onTap":
+        final String markerId = call.arguments['marker'];
+        final Marker marker = _markers[markerId];
+        if (marker != null) {
+          onMarkerTapped(marker);
+        }
+        break;
+      case "map#onCameraMoveStarted":
+        _isCameraMoving = true;
+        notifyListeners();
+        break;
+      case "map#onCameraMove":
+        _cameraPosition = CameraPosition._fromJson(call.arguments['position']);
+        notifyListeners();
+        break;
+      case "map#onCameraIdle":
+        _isCameraMoving = false;
+        notifyListeners();
+        break;
+      default:
+        throw new MissingPluginException();
+    }
+  }
+
+  Future<void> updateMapOptions(GoogleMapOptions options) async {
+    assert(options != null);
     final int id = await this.id;
     await _channel.invokeMethod('setMapOptions', <String, dynamic>{
       'map': id,
       'options': options._toJson(),
     });
-  }
-
-  Future<GoogleMapOptions> getMapOptions() async {
-    final int id = await this.id;
-    final dynamic json = await _channel.invokeMethod(
-      'getMapOptions',
-      <String, dynamic>{'map': id},
-    );
-    return GoogleMapOptions._fromJson(json);
+    _options = _options._updateWith(options);
+    if (!_options.trackCameraPosition) {
+      _cameraPosition = null;
+    }
+    notifyListeners();
   }
 
   Future<void> animateCamera(CameraUpdate cameraUpdate) async {
@@ -54,31 +126,62 @@
     });
   }
 
-  Future<Marker> addMarker(MarkerOptions markerOptions) async {
+  Future<Marker> addMarker(MarkerOptions options) async {
+    assert(options != null);
+    assert(options.position != null);
     final int id = await this.id;
+    final MarkerOptions effectiveOptions =
+        MarkerOptions.defaultOptions._updateWith(options);
     final String markerId = await _channel.invokeMethod(
       'addMarker',
       <String, dynamic>{
         'map': id,
-        'markerOptions': markerOptions._toJson(),
+        'options': effectiveOptions._toJson(),
       },
     );
-    return new Marker._(id, markerId, markerOptions);
+    final Marker marker = new Marker._(this, markerId, effectiveOptions);
+    _markers[markerId] = marker;
+    notifyListeners();
+    return marker;
+  }
+
+  Future<void> _updateMarker(Marker marker, MarkerOptions changes) async {
+    assert(_markers[marker.id] == marker);
+    assert(changes != null);
+    final int id = await this.id;
+    await _channel.invokeMethod('marker#update', <String, dynamic>{
+      'map': id,
+      'marker': marker.id,
+      'options': changes._toJson(),
+    });
+    marker._options = marker._options._updateWith(changes);
+    notifyListeners();
+  }
+
+  Future<void> _removeMarker(Marker marker) async {
+    assert(_markers[marker.id] == marker);
+    final int id = await this.id;
+    await _channel.invokeMethod('marker#remove', <String, dynamic>{
+      'map': id,
+      'marker': marker.id,
+    });
+    _markers.remove(marker.id);
+    notifyListeners();
   }
 }
 
-/// Controller for a GoogleMaps instance that is integrated as a
+/// Controller for a GoogleMap instance that is integrated as a
 /// platform overlay.
 ///
 /// *Warning*: Platform overlays cannot be freely composed with
 /// other widgets. See [PlatformOverlayController] for caveats and
 /// limitations.
-class GoogleMapsOverlayController {
-  GoogleMapsOverlayController._(this.mapsController, this.overlayController);
+class GoogleMapOverlayController {
+  GoogleMapOverlayController._(this.mapController, this.overlayController);
 
   /// Creates a controller for a GoogleMaps of the specified size in
   /// logical pixels.
-  factory GoogleMapsOverlayController.fromSize({
+  factory GoogleMapOverlayController.fromSize({
     @required double width,
     @required double height,
     GoogleMapOptions options = const GoogleMapOptions(),
@@ -86,16 +189,21 @@
     assert(width != null);
     assert(height != null);
     assert(options != null);
+    final GoogleMapOptions effectiveOptions =
+        GoogleMapOptions.defaultOptions._updateWith(options);
     final _GoogleMapsPlatformOverlay overlay =
-        new _GoogleMapsPlatformOverlay(options);
-    return new GoogleMapsOverlayController._(
-      new GoogleMapsController(overlay._textureId.future),
+        new _GoogleMapsPlatformOverlay(effectiveOptions);
+    return new GoogleMapOverlayController._(
+      new GoogleMapController._(
+        id: overlay._textureId.future,
+        options: effectiveOptions,
+      ),
       new PlatformOverlayController(width, height, overlay),
     );
   }
 
   /// The controller of the GoogleMaps instance.
-  final GoogleMapsController mapsController;
+  final GoogleMapController mapController;
 
   /// The controller of the platform overlay.
   final PlatformOverlayController overlayController;
@@ -149,16 +257,16 @@
 }
 
 /// A Widget covered by a GoogleMaps platform overlay.
-class GoogleMapsOverlay extends StatefulWidget {
-  final GoogleMapsOverlayController controller;
+class GoogleMapOverlay extends StatefulWidget {
+  final GoogleMapOverlayController controller;
 
-  GoogleMapsOverlay({Key key, @required this.controller}) : super(key: key);
+  GoogleMapOverlay({Key key, @required this.controller}) : super(key: key);
 
   @override
-  State<StatefulWidget> createState() => new _GoogleMapsOverlayState();
+  State<StatefulWidget> createState() => new _GoogleMapOverlayState();
 }
 
-class _GoogleMapsOverlayState extends State<GoogleMapsOverlay> {
+class _GoogleMapOverlayState extends State<GoogleMapOverlay> {
   @override
   void initState() {
     super.initState();
@@ -175,7 +283,7 @@
   Widget build(BuildContext context) {
     return new SizedBox(
       child: new FutureBuilder<int>(
-        future: widget.controller.mapsController.id,
+        future: widget.controller.mapController.id,
         builder: (_, AsyncSnapshot<int> snapshot) {
           if (snapshot.hasData) {
             return new Texture(textureId: snapshot.data);
diff --git a/packages/google_mobile_maps/lib/src/marker.dart b/packages/google_mobile_maps/lib/src/marker.dart
index 14b9b91..a328978 100644
--- a/packages/google_mobile_maps/lib/src/marker.dart
+++ b/packages/google_mobile_maps/lib/src/marker.dart
@@ -4,127 +4,134 @@
 
 part of google_mobile_maps;
 
+/// An icon placed at a particular point on the map's surface. A marker icon is
+/// drawn oriented against the device's screen rather than the map's surface;
+/// that is, it will not necessarily change orientation due to map rotations,
+/// tilting, or zooming.
+///
+/// Markers are owned by a single [GoogleMapController] which fires change
+/// events when markers are added, updated, or removed.
 class Marker {
-  Marker._(this.mapId, this.id, MarkerOptions options) : _options = options;
+  Marker._(this._mapController, this.id, this._options);
 
-  final int mapId;
+  final GoogleMapController _mapController;
   final String id;
   MarkerOptions _options;
 
-  Future<void> update(MarkerOptions options) async {
-    assert(options != null);
-    _options = options;
-    await _channel.invokeMethod('marker#update', <String, dynamic>{
-      'map': mapId,
-      'marker': id,
-      'markerOptions': options._toJson(),
-    });
+  Future<void> remove() {
+    return _mapController._removeMarker(this);
   }
 
+  Future<void> update(MarkerOptions changes) {
+    return _mapController._updateMarker(this, changes);
+  }
+
+  /// The configuration options most recently applied programmatically.
+  ///
+  /// The returned value does not reflect any changes made to the marker through
+  /// touch events. Add listeners to track those.
   MarkerOptions get options => _options;
-
-  Future<void> remove() async {
-    await _channel.invokeMethod('marker#remove', <String, dynamic>{
-      'map': mapId,
-      'marker': id,
-    });
-  }
-
-  Future<void> hideInfoWindow() async {
-    await _channel.invokeMethod('marker#hideInfoWindow', <String, dynamic>{
-      'map': mapId,
-      'marker': id,
-    });
-  }
-
-  Future<void> showInfoWindow() async {
-    await _channel.invokeMethod('marker#showInfoWindow', <String, dynamic>{
-      'map': mapId,
-      'marker': id,
-    });
-  }
 }
 
+dynamic _offsetToJson(Offset offset) =>
+    offset == null ? null : <dynamic>[offset.dx, offset.dy];
+
+/// Text labels for a [Marker] info window.
+class InfoWindowText {
+  const InfoWindowText(this.title, this.snippet);
+
+  static const InfoWindowText noText = const InfoWindowText(null, null);
+
+  final String title;
+  final String snippet;
+
+  dynamic _toJson() => <dynamic>[title, snippet];
+}
+
+/// Configuration options for [Marker] instances.
+///
+/// When used to change configuration, null values will be interpreted as
+/// "do not change this configuration item". When used to represent current
+/// configuration, all values will be non-null.
 class MarkerOptions {
-  static const String unspecified = 'Unspecified';
-  final LatLng position;
   final double alpha;
   final Offset anchor;
+  final bool consumesTapEvents;
   final bool draggable;
   final bool flat;
   final BitmapDescriptor icon;
   final Offset infoWindowAnchor;
+  final bool infoWindowShown;
+  final InfoWindowText infoWindowText;
+  final LatLng position;
   final double rotation;
-  final String snippet;
-  final String title;
   final bool visible;
   final double zIndex;
 
   const MarkerOptions({
-    @required this.position,
-    this.alpha = 1.0,
-    this.anchor = const Offset(0.5, 1.0),
-    this.draggable = false,
-    this.flat = false,
-    this.icon = BitmapDescriptor.defaultMarker,
-    this.infoWindowAnchor = const Offset(0.5, 0.0),
-    this.rotation = 0.0,
-    this.snippet,
-    this.title,
-    this.visible = true,
-    this.zIndex = 0.0,
-  })  : assert(position != null),
-        assert(alpha != null),
-        assert(anchor != null),
-        assert(draggable != null),
-        assert(flat != null),
-        assert(icon != null),
-        assert(infoWindowAnchor != null),
-        assert(rotation != null),
-        assert(zIndex != null),
-        assert(visible != null);
+    this.alpha,
+    this.anchor,
+    this.consumesTapEvents,
+    this.draggable,
+    this.flat,
+    this.icon,
+    this.infoWindowAnchor,
+    this.infoWindowShown,
+    this.infoWindowText,
+    this.position,
+    this.rotation,
+    this.visible,
+    this.zIndex,
+  });
 
-  MarkerOptions copyWith({
-    LatLng position,
-    double alpha,
-    Offset anchor,
-    bool draggable,
-    bool flat,
-    BitmapDescriptor icon,
-    Offset infoWindowAnchor,
-    double rotation,
-    String snippet = unspecified,
-    String title = unspecified,
-    double zIndex,
-    bool visible,
-  }) =>
-      new MarkerOptions(
-        position: position ?? this.position,
-        alpha: alpha ?? this.alpha,
-        anchor: anchor ?? this.anchor,
-        draggable: draggable ?? this.draggable,
-        flat: flat ?? this.flat,
-        icon: icon ?? this.icon,
-        infoWindowAnchor: infoWindowAnchor ?? this.infoWindowAnchor,
-        rotation: rotation ?? this.rotation,
-        snippet: identical(snippet, unspecified) ? this.snippet : snippet,
-        title: identical(title, unspecified) ? this.title : title,
-        zIndex: zIndex ?? this.zIndex,
-        visible: visible ?? this.visible,
-      );
+  static const MarkerOptions defaultOptions = const MarkerOptions(
+    alpha: 1.0,
+    anchor: const Offset(0.5, 1.0),
+    consumesTapEvents: false,
+    draggable: false,
+    flat: false,
+    icon: BitmapDescriptor.defaultMarker,
+    infoWindowAnchor: const Offset(0.5, 0.0),
+    infoWindowShown: false,
+    infoWindowText: InfoWindowText.noText,
+    rotation: 0.0,
+    visible: true,
+    zIndex: 0.0,
+  );
 
-  dynamic _toJson() => <String, dynamic>{
-        'position': position._toJson(),
-        'alpha': alpha,
-        'anchor': <double>[anchor.dx, anchor.dy],
-        'draggable': draggable,
-        'flat': flat,
-        'icon': icon._toJson(),
-        'infoWindowAnchor': <double>[infoWindowAnchor.dx, infoWindowAnchor.dy],
-        'rotation': rotation,
-        'snippet': snippet,
-        'title': title,
-        'visible': visible,
-        'zIndex': zIndex,
-      };
+  MarkerOptions _updateWith(MarkerOptions changes) {
+    return new MarkerOptions(
+      alpha: changes.alpha ?? alpha,
+      anchor: changes.anchor ?? anchor,
+      consumesTapEvents: changes.consumesTapEvents ?? consumesTapEvents,
+      draggable: changes.draggable ?? draggable,
+      flat: changes.flat ?? flat,
+      icon: changes.icon ?? icon,
+      infoWindowAnchor: changes.infoWindowAnchor ?? infoWindowAnchor,
+      infoWindowShown: changes.infoWindowShown ?? infoWindowShown,
+      infoWindowText: changes.infoWindowText ?? infoWindowText,
+      position: changes.position ?? position,
+      rotation: changes.rotation ?? rotation,
+      visible: changes.visible ?? visible,
+      zIndex: changes.zIndex ?? zIndex,
+    );
+  }
+
+  dynamic _toJson() {
+    return <String, dynamic>{
+      'alpha': alpha,
+      'anchor': _offsetToJson(anchor),
+      'consumesTapEvents': consumesTapEvents,
+      'draggable': draggable,
+      'flat': flat,
+      'icon': icon?._toJson(),
+      'infoWindowAnchor': _offsetToJson(infoWindowAnchor),
+      'infoWindowShown': infoWindowShown,
+      'infoWindowText': infoWindowText?._toJson(),
+      'position': position?._toJson(),
+      'rotation': rotation,
+      'visible': visible,
+      'zIndex': zIndex,
+    };
+  }
 }
diff --git a/packages/google_mobile_maps/lib/src/platform_overlay.dart b/packages/google_mobile_maps/lib/src/platform_overlay.dart
index 8baff43..067fd67 100644
--- a/packages/google_mobile_maps/lib/src/platform_overlay.dart
+++ b/packages/google_mobile_maps/lib/src/platform_overlay.dart
@@ -4,7 +4,7 @@
 
 part of google_mobile_maps;
 
-/// Controller of platform overlays, supporting a limited form
+/// Controller of platform overlays, supporting a very limited form
 /// of compositing with Flutter Widgets.
 ///
 /// Platform overlays are normal platform-specific views that are
@@ -207,19 +207,19 @@
 
 /// Platform overlay.
 abstract class PlatformOverlay {
-  /// Create a platform view of the specified [physicalSize] (in device pixels).
+  /// Creates a platform view of the specified [physicalSize] (in device pixels).
   ///
   /// The platform view should remain hidden until explicitly shown by calling
   /// [showOverlay].
   Future<int> create(Size physicalSize);
 
-  /// Show the platform view at the specified [physicalOffset] (in device
+  /// Shows the platform view at the specified [physicalOffset] (in device
   /// pixels).
   Future<void> show(Offset physicalOffset);
 
-  /// Hide the platform view.
+  /// Hides the platform view.
   Future<void> hide();
 
-  /// Dispose of the platform view.
+  /// Disposes of the platform view.
   Future<void> dispose();
 }
diff --git a/packages/google_mobile_maps/lib/src/ui.dart b/packages/google_mobile_maps/lib/src/ui.dart
index b86744c..1323136 100644
--- a/packages/google_mobile_maps/lib/src/ui.dart
+++ b/packages/google_mobile_maps/lib/src/ui.dart
@@ -17,39 +17,22 @@
   hybrid,
 }
 
-MapType _mapTypeFromJson(dynamic json) {
-  if (json == null) {
-    return null;
-  }
-  return MapType.values[json];
-}
-
 /// Bounds for the map camera target.
-class CameraTargetBounds {
-  static const CameraTargetBounds unbounded = const CameraTargetBounds(null);
-
-  const CameraTargetBounds(this.bounds);
+class LatLngCameraTargetBounds {
+  const LatLngCameraTargetBounds(this.bounds);
 
   /// The current bounds or null, if the camera target is unbounded.
   final LatLngBounds bounds;
 
-  bool get isBounded => bounds != null;
+  static const LatLngCameraTargetBounds unbounded =
+      const LatLngCameraTargetBounds(null);
 
   dynamic _toJson() => <dynamic>[bounds?._toJson()];
-
-  static CameraTargetBounds _fromJson(dynamic json) {
-    if (json == null) {
-      return null;
-    }
-    return new CameraTargetBounds(LatLngBounds._fromJson(json[0]));
-  }
 }
 
-/// Bounds for map camera zoom level.
-class ZoomBounds {
-  static const ZoomBounds unbounded = const ZoomBounds(null, null);
-
-  const ZoomBounds(this.minZoom, this.maxZoom)
+/// Preferred bounds for map camera zoom level.
+class MinMaxZoomPreference {
+  const MinMaxZoomPreference(this.minZoom, this.maxZoom)
       : assert(minZoom == null || maxZoom == null || minZoom <= maxZoom);
 
   /// The current minimum zoom level or null, if unbounded from below.
@@ -58,16 +41,10 @@
   /// The current maximum zoom level or null, if unbounded from above.
   final double maxZoom;
 
-  bool get isBounded => minZoom != null || maxZoom != null;
+  static const MinMaxZoomPreference unbounded =
+      const MinMaxZoomPreference(null, null);
 
   dynamic _toJson() => <dynamic>[minZoom, maxZoom];
-
-  static ZoomBounds _fromJson(dynamic json) {
-    if (json == null) {
-      return null;
-    }
-    return new ZoomBounds(json[0], json[1]);
-  }
 }
 
 /// Configuration options for the GoogleMaps user interface.
@@ -77,55 +54,71 @@
 /// configuration, all values will be non-null.
 class GoogleMapOptions {
   final CameraPosition cameraPosition;
-  final CameraTargetBounds cameraTargetBounds;
   final bool compassEnabled;
+  final LatLngCameraTargetBounds latLngCameraTargetBounds;
   final MapType mapType;
+  final MinMaxZoomPreference minMaxZoomPreference;
   final bool rotateGesturesEnabled;
   final bool scrollGesturesEnabled;
   final bool tiltGesturesEnabled;
-  final ZoomBounds zoomBounds;
+  final bool trackCameraPosition;
   final bool zoomGesturesEnabled;
 
   const GoogleMapOptions({
     this.cameraPosition,
-    this.cameraTargetBounds,
     this.compassEnabled,
+    this.latLngCameraTargetBounds,
     this.mapType,
+    this.minMaxZoomPreference,
     this.rotateGesturesEnabled,
     this.scrollGesturesEnabled,
     this.tiltGesturesEnabled,
-    this.zoomBounds,
+    this.trackCameraPosition,
     this.zoomGesturesEnabled,
   });
 
-  static GoogleMapOptions _fromJson(dynamic json) {
-    if (json == null) {
-      return null;
-    }
+  static const GoogleMapOptions defaultOptions = const GoogleMapOptions(
+    compassEnabled: true,
+    latLngCameraTargetBounds: LatLngCameraTargetBounds.unbounded,
+    mapType: MapType.normal,
+    minMaxZoomPreference: MinMaxZoomPreference.unbounded,
+    rotateGesturesEnabled: true,
+    scrollGesturesEnabled: true,
+    tiltGesturesEnabled: true,
+    trackCameraPosition: false,
+    zoomGesturesEnabled: true,
+  );
+
+  GoogleMapOptions _updateWith(GoogleMapOptions change) {
     return new GoogleMapOptions(
-      cameraPosition: CameraPosition._fromJson(json['cameraPosition']),
-      cameraTargetBounds:
-          CameraTargetBounds._fromJson(json['cameraTargetBounds']),
-      compassEnabled: json['compassEnabled'],
-      mapType: _mapTypeFromJson(json['mapType']),
-      rotateGesturesEnabled: json['rotateGesturesEnabled'],
-      scrollGesturesEnabled: json['scrollGesturesEnabled'],
-      tiltGesturesEnabled: json['tiltGesturesEnabled'],
-      zoomBounds: ZoomBounds._fromJson(json['zoomBounds']),
-      zoomGesturesEnabled: json['zoomGesturesEnabled'],
+      cameraPosition: change.cameraPosition ?? cameraPosition,
+      compassEnabled: change.compassEnabled ?? compassEnabled,
+      latLngCameraTargetBounds:
+          change.latLngCameraTargetBounds ?? latLngCameraTargetBounds,
+      mapType: change.mapType ?? mapType,
+      minMaxZoomPreference: change.minMaxZoomPreference ?? minMaxZoomPreference,
+      rotateGesturesEnabled:
+          change.rotateGesturesEnabled ?? rotateGesturesEnabled,
+      scrollGesturesEnabled:
+          change.scrollGesturesEnabled ?? scrollGesturesEnabled,
+      tiltGesturesEnabled: change.tiltGesturesEnabled ?? tiltGesturesEnabled,
+      trackCameraPosition: change.trackCameraPosition ?? trackCameraPosition,
+      zoomGesturesEnabled: change.zoomGesturesEnabled ?? zoomGesturesEnabled,
     );
   }
 
   dynamic _toJson() {
     final Map<String, dynamic> json = <String, dynamic>{};
     json['cameraPosition'] = cameraPosition?._toJson();
-    json['cameraTargetBounds'] = cameraTargetBounds?._toJson();
     json['compassEnabled'] = compassEnabled;
+    json['latLngCameraTargetBounds'] = latLngCameraTargetBounds?._toJson();
     json['mapType'] = mapType?.index;
+    json['minMaxZoomPreference'] = minMaxZoomPreference?._toJson();
+    json['reportCameraMoveEvents'] = trackCameraPosition;
     json['rotateGesturesEnabled'] = rotateGesturesEnabled;
     json['scrollGesturesEnabled'] = scrollGesturesEnabled;
     json['tiltGesturesEnabled'] = tiltGesturesEnabled;
-    json['zoomBounds'] = zoomBounds?._toJson();
+    json['trackCameraPosition'] = trackCameraPosition;
     json['zoomGesturesEnabled'] = zoomGesturesEnabled;
     return json;
   }