Control the GoogleMap options with widget parameters. (#961)

The current Google Maps API was completely controller based (it was
designed before the platform views support).
Now that the GoogleMap is a widget, we are re-designing the API
around the GoogleMap widget.

This PR removes the public GoogleMapOptions class, and moves the map
options to be widget parameters. When the widget is rebuilt we compute
the options delta and send an update over the method channel.

The `initialCameraPosition` parameter was moved out of the
GoogleMapOptions Android and iOS implementations as we do not update
it when the map options are updated.

Additional API tweaks in this change:
  * Make `initialCameraPosition` a required parameter for the `GoogleMap`
    widget.
  * Don't require an onMapCreated parameter for `GoogleMap`.
diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md
index e337e7f..c703bc1 100644
--- a/packages/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.1.0
+
+* Move the map options from the GoogleMapOptions class to GoogleMap widget parameters.
+
 ## 0.0.3+3
 
 * Relax Flutter version requirement to 0.11.9.
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
index b9314eb..cacb745 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
@@ -46,7 +46,7 @@
     return (Boolean) o;
   }
 
-  private static CameraPosition toCameraPosition(Object o) {
+  static CameraPosition toCameraPosition(Object o) {
     final Map<?, ?> data = toMap(o);
     final CameraPosition.Builder builder = CameraPosition.builder();
     builder.bearing(toFloat(data.get("bearing")));
@@ -165,10 +165,6 @@
 
   static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) {
     final Map<?, ?> data = toMap(o);
-    final Object cameraPosition = data.get("cameraPosition");
-    if (cameraPosition != null) {
-      sink.setCameraPosition(toCameraPosition(cameraPosition));
-    }
     final Object cameraTargetBounds = data.get("cameraTargetBounds");
     if (cameraTargetBounds != null) {
       final List<?> targetData = toList(cameraTargetBounds);
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
index 64d5533..ea2b90a 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
@@ -26,8 +26,7 @@
     return controller;
   }
 
-  @Override
-  public void setCameraPosition(CameraPosition position) {
+  public void setInitialCameraPosition(CameraPosition position) {
     options.camera(position);
   }
 
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
index 2eebcb2..79e34bd 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -20,7 +20,6 @@
 import android.util.Log;
 import android.view.View;
 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;
@@ -353,11 +352,6 @@
   // GoogleMapOptionsSink methods
 
   @Override
-  public void setCameraPosition(CameraPosition position) {
-    googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(position));
-  }
-
-  @Override
   public void setCameraTargetBounds(LatLngBounds bounds) {
     googleMap.setLatLngBoundsForCameraTarget(bounds);
   }
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
index 8e2d18e..5d85ad2 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
@@ -3,9 +3,11 @@
 import static io.flutter.plugin.common.PluginRegistry.Registrar;
 
 import android.content.Context;
+import com.google.android.gms.maps.model.CameraPosition;
 import io.flutter.plugin.common.StandardMessageCodec;
 import io.flutter.plugin.platform.PlatformView;
 import io.flutter.plugin.platform.PlatformViewFactory;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
 public class GoogleMapFactory extends PlatformViewFactory {
@@ -20,9 +22,15 @@
   }
 
   @Override
-  public PlatformView create(Context context, int id, Object params) {
+  public PlatformView create(Context context, int id, Object args) {
+    Map<String, Object> params = (Map<String, Object>) args;
     final GoogleMapBuilder builder = new GoogleMapBuilder();
-    Convert.interpretGoogleMapOptions(params, builder);
+
+    Convert.interpretGoogleMapOptions(params.get("options"), builder);
+    if (params.containsKey("initialCameraPosition")) {
+      CameraPosition position = Convert.toCameraPosition(params.get("initialCameraPosition"));
+      builder.setInitialCameraPosition(position);
+    }
     return builder.build(id, context, mActivityState, mPluginRegistrar);
   }
 }
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
index 03b67aa..3a166bf 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
@@ -4,13 +4,10 @@
 
 package io.flutter.plugins.googlemaps;
 
-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 setCameraTargetBounds(LatLngBounds bounds);
 
   void setCompassEnabled(boolean compassEnabled);
diff --git a/packages/google_maps_flutter/example/lib/animate_camera.dart b/packages/google_maps_flutter/example/lib/animate_camera.dart
index c4bc067..fe4283d 100644
--- a/packages/google_maps_flutter/example/lib/animate_camera.dart
+++ b/packages/google_maps_flutter/example/lib/animate_camera.dart
@@ -38,11 +38,14 @@
       children: <Widget>[
         Center(
           child: SizedBox(
-              width: 300.0,
-              height: 200.0,
-              child: GoogleMap(
-                  onMapCreated: _onMapCreated,
-                  options: GoogleMapOptions.defaultOptions)),
+            width: 300.0,
+            height: 200.0,
+            child: GoogleMap(
+              onMapCreated: _onMapCreated,
+              initialCameraPosition:
+                  const CameraPosition(target: LatLng(0.0, 0.0)),
+            ),
+          ),
         ),
         Row(
           mainAxisAlignment: MainAxisAlignment.spaceEvenly,
diff --git a/packages/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/example/lib/map_ui.dart
index 589fa2b..993bddd 100644
--- a/packages/google_maps_flutter/example/lib/map_ui.dart
+++ b/packages/google_maps_flutter/example/lib/map_ui.dart
@@ -31,17 +31,23 @@
 class MapUiBodyState extends State<MapUiBody> {
   MapUiBodyState();
 
-  GoogleMapController mapController;
-  CameraPosition _position;
-  GoogleMapOptions _options = GoogleMapOptions(
-    cameraPosition: const CameraPosition(
-      target: LatLng(-33.852, 151.211),
-      zoom: 11.0,
-    ),
-    trackCameraPosition: true,
-    compassEnabled: true,
+  static final CameraPosition _kInitialPosition = const CameraPosition(
+    target: LatLng(-33.852, 151.211),
+    zoom: 11.0,
   );
+
+  GoogleMapController mapController;
+  CameraPosition _position = _kInitialPosition;
   bool _isMoving = false;
+  bool _compassEnabled = true;
+  CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded;
+  MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded;
+  MapType _mapType = MapType.normal;
+  bool _rotateGesturesEnabled = true;
+  bool _scrollGesturesEnabled = true;
+  bool _tiltGesturesEnabled = true;
+  bool _zoomGesturesEnabled = true;
+  bool _myLocationEnabled = true;
 
   @override
   void initState() {
@@ -55,7 +61,6 @@
   }
 
   void _extractMapInfo() {
-    _options = mapController.options;
     _position = mapController.cameraPosition;
     _isMoving = mapController.isCameraMoving;
   }
@@ -68,11 +73,11 @@
 
   Widget _compassToggler() {
     return FlatButton(
-      child: Text('${_options.compassEnabled ? 'disable' : 'enable'} compass'),
+      child: Text('${_compassEnabled ? 'disable' : 'enable'} compass'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(compassEnabled: !_options.compassEnabled),
-        );
+        setState(() {
+          _compassEnabled = !_compassEnabled;
+        });
       },
     );
   }
@@ -80,124 +85,120 @@
   Widget _latLngBoundsToggler() {
     return FlatButton(
       child: Text(
-        _options.cameraTargetBounds.bounds == null
+        _cameraTargetBounds.bounds == null
             ? 'bound camera target'
             : 'release camera target',
       ),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(
-            cameraTargetBounds: _options.cameraTargetBounds.bounds == null
-                ? CameraTargetBounds(sydneyBounds)
-                : CameraTargetBounds.unbounded,
-          ),
-        );
+        setState(() {
+          _cameraTargetBounds = _cameraTargetBounds.bounds == null
+              ? CameraTargetBounds(sydneyBounds)
+              : CameraTargetBounds.unbounded;
+        });
       },
     );
   }
 
   Widget _zoomBoundsToggler() {
     return FlatButton(
-      child: Text(_options.minMaxZoomPreference.minZoom == null
+      child: Text(_minMaxZoomPreference.minZoom == null
           ? 'bound zoom'
           : 'release zoom'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(
-            minMaxZoomPreference: _options.minMaxZoomPreference.minZoom == null
-                ? const MinMaxZoomPreference(12.0, 16.0)
-                : MinMaxZoomPreference.unbounded,
-          ),
-        );
+        setState(() {
+          _minMaxZoomPreference = _minMaxZoomPreference.minZoom == null
+              ? const MinMaxZoomPreference(12.0, 16.0)
+              : MinMaxZoomPreference.unbounded;
+        });
       },
     );
   }
 
   Widget _mapTypeCycler() {
     final MapType nextType =
-        MapType.values[(_options.mapType.index + 1) % MapType.values.length];
+        MapType.values[(_mapType.index + 1) % MapType.values.length];
     return FlatButton(
       child: Text('change map type to $nextType'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(mapType: nextType),
-        );
+        setState(() {
+          _mapType = nextType;
+        });
       },
     );
   }
 
   Widget _rotateToggler() {
     return FlatButton(
-      child: Text(
-          '${_options.rotateGesturesEnabled ? 'disable' : 'enable'} rotate'),
+      child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(
-            rotateGesturesEnabled: !_options.rotateGesturesEnabled,
-          ),
-        );
+        setState(() {
+          _rotateGesturesEnabled = !_rotateGesturesEnabled;
+        });
       },
     );
   }
 
   Widget _scrollToggler() {
     return FlatButton(
-      child: Text(
-          '${_options.scrollGesturesEnabled ? 'disable' : 'enable'} scroll'),
+      child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(
-            scrollGesturesEnabled: !_options.scrollGesturesEnabled,
-          ),
-        );
+        setState(() {
+          _scrollGesturesEnabled = !_scrollGesturesEnabled;
+        });
       },
     );
   }
 
   Widget _tiltToggler() {
     return FlatButton(
-      child:
-          Text('${_options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'),
+      child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(
-            tiltGesturesEnabled: !_options.tiltGesturesEnabled,
-          ),
-        );
+        setState(() {
+          _tiltGesturesEnabled = !_tiltGesturesEnabled;
+        });
       },
     );
   }
 
   Widget _zoomToggler() {
     return FlatButton(
-      child:
-          Text('${_options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'),
+      child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(
-            zoomGesturesEnabled: !_options.zoomGesturesEnabled,
-          ),
-        );
+        setState(() {
+          _zoomGesturesEnabled = !_zoomGesturesEnabled;
+        });
       },
     );
   }
 
   Widget _myLocationToggler() {
     return FlatButton(
-      child: Text(
-          '${_options.myLocationEnabled ? 'disable' : 'enable'} my location'),
+      child: Text('${_myLocationEnabled ? 'disable' : 'enable'} my location'),
       onPressed: () {
-        mapController.updateMapOptions(
-          GoogleMapOptions(
-            myLocationEnabled: !_options.myLocationEnabled,
-          ),
-        );
+        setState(() {
+          _myLocationEnabled = !_myLocationEnabled;
+        });
       },
     );
   }
 
   @override
   Widget build(BuildContext context) {
+    final GoogleMap googleMap = GoogleMap(
+      onMapCreated: onMapCreated,
+      initialCameraPosition: _kInitialPosition,
+      trackCameraPosition: true,
+      compassEnabled: _compassEnabled,
+      cameraTargetBounds: _cameraTargetBounds,
+      minMaxZoomPreference: _minMaxZoomPreference,
+      mapType: _mapType,
+      rotateGesturesEnabled: _rotateGesturesEnabled,
+      scrollGesturesEnabled: _scrollGesturesEnabled,
+      tiltGesturesEnabled: _tiltGesturesEnabled,
+      zoomGesturesEnabled: _zoomGesturesEnabled,
+      myLocationEnabled: _myLocationEnabled,
+    );
+
     final List<Widget> columnChildren = <Widget>[
       Padding(
         padding: const EdgeInsets.all(10.0),
@@ -205,16 +206,7 @@
           child: SizedBox(
             width: 300.0,
             height: 200.0,
-            child: GoogleMap(
-              onMapCreated: onMapCreated,
-              options: GoogleMapOptions(
-                cameraPosition: const CameraPosition(
-                  target: LatLng(-33.852, 151.211),
-                  zoom: 11.0,
-                ),
-                trackCameraPosition: true,
-              ),
-            ),
+            child: googleMap,
           ),
         ),
       ),
diff --git a/packages/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/example/lib/move_camera.dart
index 48117fc..299ac4b 100644
--- a/packages/google_maps_flutter/example/lib/move_camera.dart
+++ b/packages/google_maps_flutter/example/lib/move_camera.dart
@@ -37,11 +37,14 @@
       children: <Widget>[
         Center(
           child: SizedBox(
-              width: 300.0,
-              height: 200.0,
-              child: GoogleMap(
-                  onMapCreated: _onMapCreated,
-                  options: GoogleMapOptions.defaultOptions)),
+            width: 300.0,
+            height: 200.0,
+            child: GoogleMap(
+              onMapCreated: _onMapCreated,
+              initialCameraPosition:
+                  const CameraPosition(target: LatLng(0.0, 0.0)),
+            ),
+          ),
         ),
         Row(
           mainAxisAlignment: MainAxisAlignment.spaceEvenly,
diff --git a/packages/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/example/lib/place_marker.dart
index 1fb8a84..58cefb0 100644
--- a/packages/google_maps_flutter/example/lib/place_marker.dart
+++ b/packages/google_maps_flutter/example/lib/place_marker.dart
@@ -175,11 +175,9 @@
             height: 200.0,
             child: GoogleMap(
               onMapCreated: _onMapCreated,
-              options: GoogleMapOptions(
-                cameraPosition: const CameraPosition(
-                  target: LatLng(-33.852, 151.211),
-                  zoom: 11.0,
-                ),
+              initialCameraPosition: const CameraPosition(
+                target: LatLng(-33.852, 151.211),
+                zoom: 11.0,
               ),
             ),
           ),
diff --git a/packages/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/example/lib/scrolling_map.dart
index 88f7530..0ec2718 100644
--- a/packages/google_maps_flutter/example/lib/scrolling_map.dart
+++ b/packages/google_maps_flutter/example/lib/scrolling_map.dart
@@ -43,11 +43,9 @@
                     height: 300.0,
                     child: GoogleMap(
                       onMapCreated: onMapCreated,
-                      options: GoogleMapOptions(
-                        cameraPosition: CameraPosition(
-                          target: center,
-                          zoom: 11.0,
-                        ),
+                      initialCameraPosition: CameraPosition(
+                        target: center,
+                        zoom: 11.0,
                       ),
                       gestureRecognizers:
                           <Factory<OneSequenceGestureRecognizer>>[
@@ -79,11 +77,9 @@
                     height: 300.0,
                     child: GoogleMap(
                       onMapCreated: onMapCreated,
-                      options: GoogleMapOptions(
-                        cameraPosition: CameraPosition(
-                          target: center,
-                          zoom: 11.0,
-                        ),
+                      initialCameraPosition: CameraPosition(
+                        target: center,
+                        zoom: 11.0,
                       ),
                       gestureRecognizers:
                           <Factory<OneSequenceGestureRecognizer>>[
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
index ae3cff0..572d96a 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
@@ -8,7 +8,6 @@
 
 // Defines map UI options writable from Flutter.
 @protocol FLTGoogleMapOptionsSink
-- (void)setCamera:(GMSCameraPosition*)camera;
 - (void)setCameraTargetBounds:(GMSCoordinateBounds*)bounds;
 - (void)setCompassEnabled:(BOOL)enabled;
 - (void)setMapType:(GMSMapViewType)type;
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
index 50786b6..acd99fa 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
@@ -58,11 +58,11 @@
   if ([super init]) {
     _viewId = viewId;
 
-    GMSCameraPosition* camera = toOptionalCameraPosition(args[@"cameraPosition"]);
+    GMSCameraPosition* camera = toOptionalCameraPosition(args[@"initialCameraPosition"]);
     _mapView = [GMSMapView mapWithFrame:frame camera:camera];
     _markers = [NSMutableDictionary dictionaryWithCapacity:1];
     _trackCameraPosition = NO;
-    interpretMapOptions(args, self);
+    interpretMapOptions(args[@"options"], self);
     NSString* channelName =
         [NSString stringWithFormat:@"plugins.flutter.io/google_maps_%lld", viewId];
     _channel = [FlutterMethodChannel methodChannelWithName:channelName
@@ -346,10 +346,6 @@
 
 static void interpretMapOptions(id json, id<FLTGoogleMapOptionsSink> sink) {
   NSDictionary* data = json;
-  id cameraPosition = data[@"cameraPosition"];
-  if (cameraPosition) {
-    [sink setCamera:toCameraPosition(cameraPosition)];
-  }
   id cameraTargetBounds = data[@"cameraTargetBounds"];
   if (cameraTargetBounds) {
     [sink setCameraTargetBounds:toOptionalBounds(cameraTargetBounds)];
diff --git a/packages/google_maps_flutter/lib/src/camera.dart b/packages/google_maps_flutter/lib/src/camera.dart
index 804d39c..ceb4289 100644
--- a/packages/google_maps_flutter/lib/src/camera.dart
+++ b/packages/google_maps_flutter/lib/src/camera.dart
@@ -51,14 +51,15 @@
   /// will be silently clamped to the supported range.
   final double zoom;
 
-  dynamic _toJson() => <String, dynamic>{
+  dynamic _toMap() => <String, dynamic>{
         'bearing': bearing,
         'target': target._toJson(),
         'tilt': tilt,
         'zoom': zoom,
       };
 
-  static CameraPosition _fromJson(dynamic json) {
+  @visibleForTesting
+  static CameraPosition fromMap(dynamic json) {
     if (json == null) {
       return null;
     }
@@ -69,6 +70,24 @@
       zoom: json['zoom'],
     );
   }
+
+  @override
+  bool operator ==(dynamic other) {
+    if (identical(this, other)) return true;
+    if (runtimeType != other.runtimeType) return false;
+    final CameraPosition typedOther = other;
+    return bearing == typedOther.bearing &&
+        target == typedOther.target &&
+        tilt == typedOther.tilt &&
+        zoom == typedOther.zoom;
+  }
+
+  @override
+  int get hashCode => hashValues(bearing, target, tilt, zoom);
+
+  @override
+  String toString() =>
+      'CameraPosition(bearing: $bearing, target: $target, tilt: $tilt, zoom: $zoom)';
 }
 
 /// Defines a camera move, supporting absolute moves as well as moves relative
@@ -79,7 +98,7 @@
   /// Returns a camera update that moves the camera to the specified position.
   static CameraUpdate newCameraPosition(CameraPosition cameraPosition) {
     return CameraUpdate._(
-      <dynamic>['newCameraPosition', cameraPosition._toJson()],
+      <dynamic>['newCameraPosition', cameraPosition._toMap()],
     );
   }
 
@@ -96,7 +115,7 @@
   static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) {
     return CameraUpdate._(<dynamic>[
       'newLatLngBounds',
-      bounds._toJson(),
+      bounds._toList(),
       padding,
     ]);
   }
diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart
index 629ab2a..ee25ac2 100644
--- a/packages/google_maps_flutter/lib/src/controller.dart
+++ b/packages/google_maps_flutter/lib/src/controller.dart
@@ -18,28 +18,21 @@
 /// Marker tap events can be received by adding callbacks to [onMarkerTapped].
 class GoogleMapController extends ChangeNotifier {
   GoogleMapController._(
-      this._id, GoogleMapOptions options, MethodChannel channel)
+      this._id, MethodChannel channel, CameraPosition initialCameraPosition)
       : assert(_id != null),
-        assert(options != null),
-        assert(options.cameraPosition != null),
         assert(channel != null),
         _channel = channel {
-    if (options.trackCameraPosition) {
-      _cameraPosition = options.cameraPosition;
-    }
+    _cameraPosition = initialCameraPosition;
     _channel.setMethodCallHandler(_handleMethodCall);
-    _options = GoogleMapOptions.defaultOptions.copyWith(options);
   }
 
   static Future<GoogleMapController> init(
-      int id, GoogleMapOptions options) async {
+      int id, CameraPosition initialCameraPosition) async {
     assert(id != null);
-    assert(options != null);
-    assert(options.cameraPosition != null);
     final MethodChannel channel =
         MethodChannel('plugins.flutter.io/google_maps_$id');
     await channel.invokeMethod('map#waitForMap');
-    return GoogleMapController._(id, options, channel);
+    return GoogleMapController._(id, channel, initialCameraPosition);
   }
 
   final MethodChannel _channel;
@@ -51,11 +44,6 @@
   final ArgumentCallbacks<Marker> onInfoWindowTapped =
       ArgumentCallbacks<Marker>();
 
-  /// The configuration options most recently applied via controller
-  /// initialization or [updateMapOptions].
-  GoogleMapOptions get options => _options;
-  GoogleMapOptions _options;
-
   /// The current set of markers on this map.
   ///
   /// The returned set will be a detached snapshot of the markers collection.
@@ -67,8 +55,7 @@
   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].
+  /// Will be null, if [GoogleMap.trackCameraPosition] is false.
   CameraPosition get cameraPosition => _cameraPosition;
   CameraPosition _cameraPosition;
 
@@ -96,7 +83,7 @@
         notifyListeners();
         break;
       case 'camera#onMove':
-        _cameraPosition = CameraPosition._fromJson(call.arguments['position']);
+        _cameraPosition = CameraPosition.fromMap(call.arguments['position']);
         notifyListeners();
         break;
       case 'camera#onIdle':
@@ -114,16 +101,15 @@
   /// platform side.
   ///
   /// The returned [Future] completes after listeners have been notified.
-  Future<void> updateMapOptions(GoogleMapOptions changes) async {
-    assert(changes != null);
+  Future<void> _updateMapOptions(Map<String, dynamic> optionsUpdate) async {
+    assert(optionsUpdate != null);
     final dynamic json = await _channel.invokeMethod(
       'map#update',
       <String, dynamic>{
-        'options': changes._toJson(),
+        'options': optionsUpdate,
       },
     );
-    _options = _options.copyWith(changes);
-    _cameraPosition = CameraPosition._fromJson(json);
+    _cameraPosition = CameraPosition.fromMap(json);
     notifyListeners();
   }
 
diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart
index 3f4298e..a1f1ad4 100644
--- a/packages/google_maps_flutter/lib/src/google_map.dart
+++ b/packages/google_maps_flutter/lib/src/google_map.dart
@@ -7,15 +7,80 @@
 typedef void MapCreatedCallback(GoogleMapController controller);
 
 class GoogleMap extends StatefulWidget {
-  GoogleMap({
-    @required this.onMapCreated,
-    GoogleMapOptions options,
+  const GoogleMap({
+    @required this.initialCameraPosition,
+    this.onMapCreated,
     this.gestureRecognizers,
-  }) : options = GoogleMapOptions.defaultOptions.copyWith(options);
+    this.compassEnabled = true,
+    this.cameraTargetBounds = CameraTargetBounds.unbounded,
+    this.mapType = MapType.normal,
+    this.minMaxZoomPreference = MinMaxZoomPreference.unbounded,
+    this.rotateGesturesEnabled = true,
+    this.scrollGesturesEnabled = true,
+    this.zoomGesturesEnabled = true,
+    this.tiltGesturesEnabled = true,
+    this.trackCameraPosition = false,
+    this.myLocationEnabled = false,
+  }) : assert(initialCameraPosition != null);
 
   final MapCreatedCallback onMapCreated;
 
-  final GoogleMapOptions options;
+  /// The initial position of the map's camera.
+  final CameraPosition initialCameraPosition;
+
+  /// True if the map should show a compass when rotated.
+  final bool compassEnabled;
+
+  /// Geographical bounding box for the camera target.
+  final CameraTargetBounds cameraTargetBounds;
+
+  /// Type of map tiles to be rendered.
+  final MapType mapType;
+
+  /// Preferred bounds for the camera zoom level.
+  ///
+  /// Actual bounds depend on map data and device.
+  final MinMaxZoomPreference minMaxZoomPreference;
+
+  /// True if the map view should respond to rotate gestures.
+  final bool rotateGesturesEnabled;
+
+  /// True if the map view should respond to scroll gestures.
+  final bool scrollGesturesEnabled;
+
+  /// True if the map view should respond to zoom gestures.
+  final bool zoomGesturesEnabled;
+
+  /// True if the map view should respond to tilt gestures.
+  final bool tiltGesturesEnabled;
+
+  /// True if the map view should relay camera move events to Flutter.
+  final bool trackCameraPosition;
+
+  /// True if a "My Location" layer should be shown on the map.
+  ///
+  /// This layer includes a location indicator at the current device location,
+  /// as well as a My Location button.
+  /// * The indicator is a small blue dot if the device is stationary, or a
+  /// chevron if the device is moving.
+  /// * The My Location button animates to focus on the user's current location
+  /// if the user's location is currently known.
+  ///
+  /// Enabling this feature requires adding location permissions to both native
+  /// platforms of your app.
+  /// * On Android add either
+  /// `<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />`
+  /// or `<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />`
+  /// to your `AndroidManifest.xml` file. `ACCESS_COARSE_LOCATION` returns a
+  /// location with an accuracy approximately equivalent to a city block, while
+  /// `ACCESS_FINE_LOCATION` returns as precise a location as possible, although
+  /// it consumes more battery power. You will also need to request these
+  /// permissions during run-time. If they are not granted, the My Location
+  /// feature will fail silently.
+  /// * On iOS add a `NSLocationWhenInUseUsageDescription` key to your
+  /// `Info.plist` file. This will automatically prompt the user for permissions
+  /// when the map tries to turn on the My Location layer.
+  final bool myLocationEnabled;
 
   /// Which gestures should be consumed by the map.
   ///
@@ -33,14 +98,23 @@
 }
 
 class _GoogleMapState extends State<GoogleMap> {
+  final Completer<GoogleMapController> _controller =
+      Completer<GoogleMapController>();
+
+  _GoogleMapOptions _googleMapOptions;
+
   @override
   Widget build(BuildContext context) {
+    final Map<String, dynamic> creationParams = <String, dynamic>{
+      'initialCameraPosition': widget.initialCameraPosition?._toMap(),
+      'options': _GoogleMapOptions.fromWidget(widget).toMap(),
+    };
     if (defaultTargetPlatform == TargetPlatform.android) {
       return AndroidView(
         viewType: 'plugins.flutter.io/google_maps',
         onPlatformViewCreated: onPlatformViewCreated,
         gestureRecognizers: widget.gestureRecognizers,
-        creationParams: widget.options._toJson(),
+        creationParams: creationParams,
         creationParamsCodec: const StandardMessageCodec(),
       );
     } else if (defaultTargetPlatform == TargetPlatform.iOS) {
@@ -48,7 +122,7 @@
         viewType: 'plugins.flutter.io/google_maps',
         onPlatformViewCreated: onPlatformViewCreated,
         gestureRecognizers: widget.gestureRecognizers,
-        creationParams: widget.options._toJson(),
+        creationParams: creationParams,
         creationParamsCodec: const StandardMessageCodec(),
       );
     }
@@ -57,9 +131,119 @@
         '$defaultTargetPlatform is not yet supported by the maps plugin');
   }
 
+  @override
+  void initState() {
+    super.initState();
+    _googleMapOptions = _GoogleMapOptions.fromWidget(widget);
+  }
+
+  @override
+  void didUpdateWidget(GoogleMap oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    final _GoogleMapOptions newOptions = _GoogleMapOptions.fromWidget(widget);
+    final Map<String, dynamic> updates =
+        _googleMapOptions.updatesMap(newOptions);
+    _updateOptions(updates);
+    _googleMapOptions = newOptions;
+  }
+
+  void _updateOptions(Map<String, dynamic> updates) async {
+    if (updates.isEmpty) {
+      return;
+    }
+    final GoogleMapController controller = await _controller.future;
+    controller._updateMapOptions(updates);
+  }
+
   Future<void> onPlatformViewCreated(int id) async {
     final GoogleMapController controller =
-        await GoogleMapController.init(id, widget.options);
-    widget.onMapCreated(controller);
+        await GoogleMapController.init(id, widget.initialCameraPosition);
+    _controller.complete(controller);
+    if (widget.onMapCreated != null) {
+      widget.onMapCreated(controller);
+    }
+  }
+}
+
+/// Configuration options for the GoogleMaps user interface.
+///
+/// When used to change configuration, null values will be interpreted as
+/// "do not change this configuration option".
+class _GoogleMapOptions {
+  _GoogleMapOptions({
+    this.compassEnabled,
+    this.cameraTargetBounds,
+    this.mapType,
+    this.minMaxZoomPreference,
+    this.rotateGesturesEnabled,
+    this.scrollGesturesEnabled,
+    this.tiltGesturesEnabled,
+    this.trackCameraPosition,
+    this.zoomGesturesEnabled,
+    this.myLocationEnabled,
+  });
+
+  static _GoogleMapOptions fromWidget(GoogleMap map) {
+    return _GoogleMapOptions(
+      compassEnabled: map.compassEnabled,
+      cameraTargetBounds: map.cameraTargetBounds,
+      mapType: map.mapType,
+      minMaxZoomPreference: map.minMaxZoomPreference,
+      rotateGesturesEnabled: map.rotateGesturesEnabled,
+      scrollGesturesEnabled: map.scrollGesturesEnabled,
+      tiltGesturesEnabled: map.tiltGesturesEnabled,
+      trackCameraPosition: map.trackCameraPosition,
+      zoomGesturesEnabled: map.zoomGesturesEnabled,
+      myLocationEnabled: map.myLocationEnabled,
+    );
+  }
+
+  final bool compassEnabled;
+
+  final CameraTargetBounds cameraTargetBounds;
+
+  final MapType mapType;
+
+  final MinMaxZoomPreference minMaxZoomPreference;
+
+  final bool rotateGesturesEnabled;
+
+  final bool scrollGesturesEnabled;
+
+  final bool tiltGesturesEnabled;
+
+  final bool trackCameraPosition;
+
+  final bool zoomGesturesEnabled;
+
+  final bool myLocationEnabled;
+
+  Map<String, dynamic> toMap() {
+    final Map<String, dynamic> optionsMap = <String, dynamic>{};
+
+    void addIfNonNull(String fieldName, dynamic value) {
+      if (value != null) {
+        optionsMap[fieldName] = value;
+      }
+    }
+
+    addIfNonNull('compassEnabled', compassEnabled);
+    addIfNonNull('cameraTargetBounds', cameraTargetBounds?._toJson());
+    addIfNonNull('mapType', mapType?.index);
+    addIfNonNull('minMaxZoomPreference', minMaxZoomPreference?._toJson());
+    addIfNonNull('rotateGesturesEnabled', rotateGesturesEnabled);
+    addIfNonNull('scrollGesturesEnabled', scrollGesturesEnabled);
+    addIfNonNull('tiltGesturesEnabled', tiltGesturesEnabled);
+    addIfNonNull('zoomGesturesEnabled', zoomGesturesEnabled);
+    addIfNonNull('trackCameraPosition', trackCameraPosition);
+    addIfNonNull('myLocationEnabled', myLocationEnabled);
+    return optionsMap;
+  }
+
+  Map<String, dynamic> updatesMap(_GoogleMapOptions newOptions) {
+    final Map<String, dynamic> prevOptionsMap = toMap();
+    return newOptions.toMap()
+      ..removeWhere(
+          (String key, dynamic value) => prevOptionsMap[key] == value);
   }
 }
diff --git a/packages/google_maps_flutter/lib/src/location.dart b/packages/google_maps_flutter/lib/src/location.dart
index 04aba84..d59412e 100644
--- a/packages/google_maps_flutter/lib/src/location.dart
+++ b/packages/google_maps_flutter/lib/src/location.dart
@@ -38,9 +38,7 @@
   }
 
   @override
-  String toString() {
-    return '$runtimeType[$latitude, $longitude]';
-  }
+  String toString() => '$runtimeType($latitude, $longitude)';
 
   @override
   bool operator ==(Object o) {
@@ -75,11 +73,12 @@
   /// The northeast corner of the rectangle.
   final LatLng northeast;
 
-  dynamic _toJson() {
+  dynamic _toList() {
     return <dynamic>[southwest._toJson(), northeast._toJson()];
   }
 
-  static LatLngBounds _fromJson(dynamic json) {
+  @visibleForTesting
+  static LatLngBounds fromList(dynamic json) {
     if (json == null) {
       return null;
     }
@@ -91,7 +90,7 @@
 
   @override
   String toString() {
-    return '$runtimeType[$southwest, $northeast]';
+    return '$runtimeType($southwest, $northeast)';
   }
 
   @override
diff --git a/packages/google_maps_flutter/lib/src/ui.dart b/packages/google_maps_flutter/lib/src/ui.dart
index 66cfc60..ea96f35 100644
--- a/packages/google_maps_flutter/lib/src/ui.dart
+++ b/packages/google_maps_flutter/lib/src/ui.dart
@@ -42,7 +42,23 @@
   /// Unbounded camera target.
   static const CameraTargetBounds unbounded = CameraTargetBounds(null);
 
-  dynamic _toJson() => <dynamic>[bounds?._toJson()];
+  dynamic _toJson() => <dynamic>[bounds?._toList()];
+
+  @override
+  bool operator ==(dynamic other) {
+    if (identical(this, other)) return true;
+    if (runtimeType != other.runtimeType) return false;
+    final CameraTargetBounds typedOther = other;
+    return bounds == typedOther.bounds;
+  }
+
+  @override
+  int get hashCode => bounds.hashCode;
+
+  @override
+  String toString() {
+    return 'CameraTargetBounds(bounds: $bounds)';
+  }
 }
 
 /// Preferred bounds for map camera zoom level.
@@ -64,166 +80,20 @@
       MinMaxZoomPreference(null, null);
 
   dynamic _toJson() => <dynamic>[minZoom, maxZoom];
-}
 
-/// Configuration options for the GoogleMaps user interface.
-///
-/// When used to change configuration, null values will be interpreted as
-/// "do not change this configuration option".
-class GoogleMapOptions {
-  /// Creates a set of map user interface configuration options.
-  ///
-  /// By default, every non-specified field is null, meaning no desire to change
-  /// user interface defaults or current configuration.
-  GoogleMapOptions({
-    this.cameraPosition,
-    this.compassEnabled,
-    this.cameraTargetBounds,
-    this.mapType,
-    this.minMaxZoomPreference,
-    this.rotateGesturesEnabled,
-    this.scrollGesturesEnabled,
-    this.tiltGesturesEnabled,
-    this.trackCameraPosition,
-    this.zoomGesturesEnabled,
-    this.myLocationEnabled,
-  });
-
-  /// The desired position of the map camera.
-  ///
-  /// This field is used to indicate initial camera position and to update that
-  /// position programmatically along with other changes to the map user
-  /// interface. It does not track the camera position through animations or
-  /// reflect movements caused by user touch events.
-  final CameraPosition cameraPosition;
-
-  /// True if the map should show a compass when rotated.
-  final bool compassEnabled;
-
-  /// Geographical bounding box for the camera target.
-  final CameraTargetBounds cameraTargetBounds;
-
-  /// Type of map tiles to be rendered.
-  final MapType mapType;
-
-  /// Preferred bounds for the camera zoom level.
-  ///
-  /// Actual bounds depend on map data and device.
-  final MinMaxZoomPreference minMaxZoomPreference;
-
-  /// True if the map view should respond to rotate gestures.
-  final bool rotateGesturesEnabled;
-
-  /// True if the map view should respond to scroll gestures.
-  final bool scrollGesturesEnabled;
-
-  /// True if the map view should respond to tilt gestures.
-  final bool tiltGesturesEnabled;
-
-  /// True if the map view should relay camera move events to Flutter.
-  final bool trackCameraPosition;
-
-  /// True if the map view should respond to zoom gestures.
-  final bool zoomGesturesEnabled;
-
-  /// True if a "My Location" layer should be shown on the map.
-  ///
-  /// This layer includes a location indicator at the current device location,
-  /// as well as a My Location button.
-  /// * The indicator is a small blue dot if the device is stationary, or a
-  /// chevron if the device is moving.
-  /// * The My Location button animates to focus on the user's current location
-  /// if the user's location is currently known.
-  ///
-  /// Enabling this feature requires adding location permissions to both native
-  /// platforms of your app.
-  /// * On Android add either
-  /// `<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />`
-  /// or `<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />`
-  /// to your `AndroidManifest.xml` file. `ACCESS_COARSE_LOCATION` returns a
-  /// location with an accuracy approximately equivalent to a city block, while
-  /// `ACCESS_FINE_LOCATION` returns as precise a location as possible, although
-  /// it consumes more battery power. You will also need to request these
-  /// permissions during run-time. If they are not granted, the My Location
-  /// feature will fail silently.
-  /// * On iOS add a `NSLocationWhenInUseUsageDescription` key to your
-  /// `Info.plist` file. This will automatically prompt the user for permissions
-  /// when the map tries to turn on the My Location layer.
-  final bool myLocationEnabled;
-
-  /// Default user interface options.
-  ///
-  /// Specifies a map view that
-  /// * displays a compass when rotated; [compassEnabled] is true
-  /// * positions the camera at 0,0; [cameraPosition] has target `LatLng(0.0, 0.0)`
-  /// * does not bound the camera target; [cameraTargetBounds] is `CameraTargetBounds.unbounded`
-  /// * uses normal map tiles; [mapType] is `MapType.normal`
-  /// * does not bound zooming; [minMaxZoomPreference] is `MinMaxZoomPreference.unbounded`
-  /// * responds to rotate gestures; [rotateGesturesEnabled] is true
-  /// * responds to scroll gestures; [scrollGesturesEnabled] is true
-  /// * responds to tilt gestures; [tiltGesturesEnabled] is true
-  /// * is silent about camera movement; [trackCameraPosition] is false
-  /// * responds to zoom gestures; [zoomGesturesEnabled] is true
-  /// * does not show user location; [myLocationEnabled] is false
-  static final GoogleMapOptions defaultOptions = GoogleMapOptions(
-    compassEnabled: true,
-    cameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)),
-    cameraTargetBounds: CameraTargetBounds.unbounded,
-    mapType: MapType.normal,
-    minMaxZoomPreference: MinMaxZoomPreference.unbounded,
-    rotateGesturesEnabled: true,
-    scrollGesturesEnabled: true,
-    tiltGesturesEnabled: true,
-    trackCameraPosition: false,
-    zoomGesturesEnabled: true,
-    myLocationEnabled: false,
-  );
-
-  /// Creates a new options object whose values are the same as this instance,
-  /// unless overwritten by the specified [changes].
-  ///
-  /// Returns this instance, if [changes] is null.
-  GoogleMapOptions copyWith(GoogleMapOptions change) {
-    if (change == null) {
-      return this;
-    }
-    return GoogleMapOptions(
-      cameraPosition: change.cameraPosition ?? cameraPosition,
-      compassEnabled: change.compassEnabled ?? compassEnabled,
-      cameraTargetBounds: change.cameraTargetBounds ?? cameraTargetBounds,
-      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,
-      myLocationEnabled: change.myLocationEnabled ?? myLocationEnabled,
-    );
+  @override
+  bool operator ==(dynamic other) {
+    if (identical(this, other)) return true;
+    if (runtimeType != other.runtimeType) return false;
+    final MinMaxZoomPreference typedOther = other;
+    return minZoom == typedOther.minZoom && maxZoom == typedOther.maxZoom;
   }
 
-  dynamic _toJson() {
-    final Map<String, dynamic> json = <String, dynamic>{};
+  @override
+  int get hashCode => hashValues(minZoom, maxZoom);
 
-    void addIfPresent(String fieldName, dynamic value) {
-      if (value != null) {
-        json[fieldName] = value;
-      }
-    }
-
-    addIfPresent('cameraPosition', cameraPosition?._toJson());
-    addIfPresent('compassEnabled', compassEnabled);
-    addIfPresent('cameraTargetBounds', cameraTargetBounds?._toJson());
-    addIfPresent('mapType', mapType?.index);
-    addIfPresent('minMaxZoomPreference', minMaxZoomPreference?._toJson());
-    addIfPresent('rotateGesturesEnabled', rotateGesturesEnabled);
-    addIfPresent('scrollGesturesEnabled', scrollGesturesEnabled);
-    addIfPresent('tiltGesturesEnabled', tiltGesturesEnabled);
-    addIfPresent('trackCameraPosition', trackCameraPosition);
-    addIfPresent('zoomGesturesEnabled', zoomGesturesEnabled);
-    addIfPresent('myLocationEnabled', myLocationEnabled);
-    return json;
+  @override
+  String toString() {
+    return 'MinMaxZoomPreference(minZoom: $minZoom, maxZoom: $maxZoom)';
   }
 }
diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml
index 032cc3b..b4c73f9 100644
--- a/packages/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/pubspec.yaml
@@ -2,12 +2,16 @@
 description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
 author: Flutter Team <flutter-dev@googlegroups.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter
-version: 0.0.3+3
+version: 0.1.0
 
 dependencies:
   flutter:
     sdk: flutter
 
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
 flutter:
   plugin:
     androidPackage: io.flutter.plugins.googlemaps
diff --git a/packages/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/test/google_map_test.dart
new file mode 100644
index 0000000..2f20a37
--- /dev/null
+++ b/packages/google_maps_flutter/test/google_map_test.dart
@@ -0,0 +1,499 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:typed_data';
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+void main() {
+  final _FakePlatformViewsController fakePlatformViewsController =
+      _FakePlatformViewsController();
+
+  setUpAll(() {
+    SystemChannels.platform_views.setMockMethodCallHandler(
+        fakePlatformViewsController.fakePlatformViewsMethodHandler);
+  });
+
+  setUp(() {
+    fakePlatformViewsController.reset();
+  });
+
+  testWidgets('Initial camera position', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.cameraPosition,
+        const CameraPosition(target: LatLng(10.0, 15.0)));
+  });
+
+  testWidgets('Initial camera position change is a no-op',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+        ),
+      ),
+    );
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 16.0)),
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.cameraPosition,
+        const CameraPosition(target: LatLng(10.0, 15.0)));
+  });
+
+  testWidgets('Can update compassEnabled', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          compassEnabled: false,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.compassEnabled, false);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          compassEnabled: true,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.compassEnabled, true);
+  });
+
+  testWidgets('Can update cameraTargetBounds', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition:
+              const CameraPosition(target: LatLng(10.0, 15.0)),
+          cameraTargetBounds: CameraTargetBounds(
+            LatLngBounds(
+              southwest: const LatLng(10.0, 20.0),
+              northeast: const LatLng(30.0, 40.0),
+            ),
+          ),
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(
+        platformGoogleMap.cameraTargetBounds,
+        CameraTargetBounds(
+          LatLngBounds(
+            southwest: const LatLng(10.0, 20.0),
+            northeast: const LatLng(30.0, 40.0),
+          ),
+        ));
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition:
+              const CameraPosition(target: LatLng(10.0, 15.0)),
+          cameraTargetBounds: CameraTargetBounds(
+            LatLngBounds(
+              southwest: const LatLng(16.0, 20.0),
+              northeast: const LatLng(30.0, 40.0),
+            ),
+          ),
+        ),
+      ),
+    );
+
+    expect(
+        platformGoogleMap.cameraTargetBounds,
+        CameraTargetBounds(
+          LatLngBounds(
+            southwest: const LatLng(16.0, 20.0),
+            northeast: const LatLng(30.0, 40.0),
+          ),
+        ));
+  });
+
+  testWidgets('Can update mapType', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          mapType: MapType.hybrid,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.mapType, MapType.hybrid);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          mapType: MapType.satellite,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.mapType, MapType.satellite);
+  });
+
+  testWidgets('Can update minMaxZoom', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          minMaxZoomPreference: MinMaxZoomPreference(1.0, 3.0),
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.minMaxZoomPreference,
+        const MinMaxZoomPreference(1.0, 3.0));
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          minMaxZoomPreference: MinMaxZoomPreference.unbounded,
+        ),
+      ),
+    );
+
+    expect(
+        platformGoogleMap.minMaxZoomPreference, MinMaxZoomPreference.unbounded);
+  });
+
+  testWidgets('Can update rotateGesturesEnabled', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          rotateGesturesEnabled: false,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.rotateGesturesEnabled, false);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          rotateGesturesEnabled: true,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.rotateGesturesEnabled, true);
+  });
+
+  testWidgets('Can update scrollGesturesEnabled', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          scrollGesturesEnabled: false,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.scrollGesturesEnabled, false);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          scrollGesturesEnabled: true,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.scrollGesturesEnabled, true);
+  });
+
+  testWidgets('Can update tiltGesturesEnabled', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          tiltGesturesEnabled: false,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.tiltGesturesEnabled, false);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          tiltGesturesEnabled: true,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.tiltGesturesEnabled, true);
+  });
+
+  testWidgets('Can update trackCameraPosition', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          trackCameraPosition: false,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.trackCameraPosition, false);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          trackCameraPosition: true,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.trackCameraPosition, true);
+  });
+
+  testWidgets('Can update zoomGesturesEnabled', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          zoomGesturesEnabled: false,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.zoomGesturesEnabled, false);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          zoomGesturesEnabled: true,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.zoomGesturesEnabled, true);
+  });
+
+  testWidgets('Can update myLocationEnabled', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          myLocationEnabled: false,
+        ),
+      ),
+    );
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.myLocationEnabled, false);
+
+    await tester.pumpWidget(
+      const Directionality(
+        textDirection: TextDirection.ltr,
+        child: GoogleMap(
+          initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)),
+          myLocationEnabled: true,
+        ),
+      ),
+    );
+
+    expect(platformGoogleMap.myLocationEnabled, true);
+  });
+}
+
+class FakePlatformGoogleMap {
+  FakePlatformGoogleMap(int id, Map<dynamic, dynamic> params) {
+    cameraPosition = CameraPosition.fromMap(params['initialCameraPosition']);
+    channel = MethodChannel(
+        'plugins.flutter.io/google_maps_$id', const StandardMethodCodec());
+    channel.setMockMethodCallHandler(onMethodCall);
+    updateOptions(params['options']);
+  }
+
+  MethodChannel channel;
+
+  CameraPosition cameraPosition;
+
+  bool compassEnabled;
+
+  CameraTargetBounds cameraTargetBounds;
+
+  MapType mapType;
+
+  MinMaxZoomPreference minMaxZoomPreference;
+
+  bool rotateGesturesEnabled;
+
+  bool scrollGesturesEnabled;
+
+  bool tiltGesturesEnabled;
+
+  bool zoomGesturesEnabled;
+
+  bool trackCameraPosition;
+
+  bool myLocationEnabled;
+
+  Future<dynamic> onMethodCall(MethodCall call) {
+    switch (call.method) {
+      case 'map#update':
+        updateOptions(call.arguments['options']);
+        return Future<void>.sync(() {});
+    }
+    return Future<void>.sync(() {});
+  }
+
+  void updateOptions(Map<dynamic, dynamic> options) {
+    if (options.containsKey('compassEnabled')) {
+      compassEnabled = options['compassEnabled'];
+    }
+    if (options.containsKey('cameraTargetBounds')) {
+      final List<dynamic> boundsList = options['cameraTargetBounds'];
+      cameraTargetBounds = boundsList[0] == null
+          ? CameraTargetBounds.unbounded
+          : CameraTargetBounds(LatLngBounds.fromList(boundsList[0]));
+    }
+    if (options.containsKey('mapType')) {
+      mapType = MapType.values[options['mapType']];
+    }
+    if (options.containsKey('minMaxZoomPreference')) {
+      final List<dynamic> minMaxZoomList = options['minMaxZoomPreference'];
+      minMaxZoomPreference =
+          MinMaxZoomPreference(minMaxZoomList[0], minMaxZoomList[1]);
+    }
+    if (options.containsKey('rotateGesturesEnabled')) {
+      rotateGesturesEnabled = options['rotateGesturesEnabled'];
+    }
+    if (options.containsKey('scrollGesturesEnabled')) {
+      scrollGesturesEnabled = options['scrollGesturesEnabled'];
+    }
+    if (options.containsKey('tiltGesturesEnabled')) {
+      tiltGesturesEnabled = options['tiltGesturesEnabled'];
+    }
+    if (options.containsKey('trackCameraPosition')) {
+      trackCameraPosition = options['trackCameraPosition'];
+    }
+    if (options.containsKey('zoomGesturesEnabled')) {
+      zoomGesturesEnabled = options['zoomGesturesEnabled'];
+    }
+    if (options.containsKey('myLocationEnabled')) {
+      myLocationEnabled = options['myLocationEnabled'];
+    }
+  }
+}
+
+class _FakePlatformViewsController {
+  FakePlatformGoogleMap lastCreatedView;
+
+  Future<dynamic> fakePlatformViewsMethodHandler(MethodCall call) {
+    switch (call.method) {
+      case 'create':
+        final Map<dynamic, dynamic> args = call.arguments;
+        final Map<dynamic, dynamic> params = _decodeParams(args['params']);
+        lastCreatedView = FakePlatformGoogleMap(
+          args['id'],
+          params,
+        );
+        return Future<int>.sync(() => 1);
+      default:
+        return Future<void>.sync(() {});
+    }
+  }
+
+  void reset() {
+    lastCreatedView = null;
+  }
+}
+
+Map<dynamic, dynamic> _decodeParams(Uint8List paramsMessage) {
+  final ByteBuffer buffer = paramsMessage.buffer;
+  final ByteData messageBytes = buffer.asByteData(
+    paramsMessage.offsetInBytes,
+    paramsMessage.lengthInBytes,
+  );
+  return const StandardMessageCodec().decodeMessage(messageBytes);
+}