[google_maps_flutter] Marker APIs are now widget based (Dart Changes) (#1239)

* Marker APIs are now widget based

Additional Context: Maps Plugin is in the process of being
moved away from a controller based API to a widget based api.
This is to facilitate easier state management and address a
lot of the common issues.

* use collection literals

* Address CR comments

* Revert "use collection literals"

This reverts commit 75956c2f58d2b97a90ce9b2ccfad6fd4856e155d.

* fix collection literal stuff

* Crearte a marker update handler and update TODOs

* ignore collection literals

* Fix test failures

* Move marker updates to their own chunks

* Fix failing tests

* Improved some docs and added some assertions

* Make class private

* Fix all hashCode and equals

* update formatring

* Address all the pending cr comments

* fix failing test

* Do not use => without return value

* remove factory method

* User `Marker marker` instead of `Marker m`

* Update changelog and pubspec.yaml
diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md
index d762e46..aa9b439 100644
--- a/packages/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.3.0
+
+* **Breaking change**. Changed the Marker API to be
+  widget based, it was controller based. Also changed the
+  example app to account for the same.
+
 ## 0.2.0+6
 
 * Updated the sample app in README.md.
diff --git a/packages/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/example/lib/place_marker.dart
index 58cefb0..8347900 100644
--- a/packages/google_maps_flutter/example/lib/place_marker.dart
+++ b/packages/google_maps_flutter/example/lib/place_marker.dart
@@ -25,142 +25,191 @@
   State<StatefulWidget> createState() => PlaceMarkerBodyState();
 }
 
+typedef Marker MarkerUpdateAction(Marker marker);
+
 class PlaceMarkerBodyState extends State<PlaceMarkerBody> {
   PlaceMarkerBodyState();
 
   static final LatLng center = const LatLng(-33.86711, 151.1947171);
 
   GoogleMapController controller;
-  int _markerCount = 0;
-  Marker _selectedMarker;
+  Map<MarkerId, Marker> markers = <MarkerId, Marker>{};
+  MarkerId selectedMarker;
+  int _markerIdCounter = 1;
 
   void _onMapCreated(GoogleMapController controller) {
     this.controller = controller;
-    controller.onMarkerTapped.add(_onMarkerTapped);
   }
 
   @override
   void dispose() {
-    controller?.onMarkerTapped?.remove(_onMarkerTapped);
     super.dispose();
   }
 
-  void _onMarkerTapped(Marker marker) {
-    if (_selectedMarker != null) {
-      _updateSelectedMarker(
-        const MarkerOptions(icon: BitmapDescriptor.defaultMarker),
-      );
+  void _onMarkerTapped(MarkerId markerId) {
+    final Marker tappedMarker = markers[markerId];
+    if (tappedMarker != null) {
+      setState(() {
+        if (markers.containsKey(selectedMarker)) {
+          final Marker resetOld = markers[selectedMarker]
+              .copyWith(iconParam: BitmapDescriptor.defaultMarker);
+          markers[selectedMarker] = resetOld;
+        }
+        selectedMarker = markerId;
+        final Marker newMarker = tappedMarker.copyWith(
+          iconParam: BitmapDescriptor.defaultMarkerWithHue(
+            BitmapDescriptor.hueGreen,
+          ),
+        );
+        markers[markerId] = newMarker;
+      });
     }
-    setState(() {
-      _selectedMarker = marker;
-    });
-    _updateSelectedMarker(
-      MarkerOptions(
-        icon: BitmapDescriptor.defaultMarkerWithHue(
-          BitmapDescriptor.hueGreen,
-        ),
-      ),
-    );
-  }
-
-  void _updateSelectedMarker(MarkerOptions changes) {
-    controller.updateMarker(_selectedMarker, changes);
   }
 
   void _add() {
-    controller.addMarker(MarkerOptions(
+    final int markerCount = markers.length;
+
+    if (markerCount == 12) {
+      return;
+    }
+
+    final String markerIdVal = 'marker_id_$_markerIdCounter';
+    _markerIdCounter++;
+    final MarkerId markerId = MarkerId(markerIdVal);
+
+    final Marker marker = Marker(
+      markerId: markerId,
       position: LatLng(
-        center.latitude + sin(_markerCount * pi / 6.0) / 20.0,
-        center.longitude + cos(_markerCount * pi / 6.0) / 20.0,
+        center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0,
+        center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0,
       ),
-      infoWindowText: InfoWindowText('Marker #${_markerCount + 1}', '*'),
-    ));
+      infoWindow: InfoWindow(title: markerIdVal, snippet: '*'),
+      onTap: () {
+        _onMarkerTapped(markerId);
+      },
+    );
+
     setState(() {
-      _markerCount += 1;
+      markers[markerId] = marker;
     });
   }
 
   void _remove() {
-    controller.removeMarker(_selectedMarker);
     setState(() {
-      _selectedMarker = null;
-      _markerCount -= 1;
+      if (markers.containsKey(selectedMarker)) {
+        markers.remove(selectedMarker);
+      }
     });
   }
 
   void _changePosition() {
-    final LatLng current = _selectedMarker.options.position;
+    final Marker marker = markers[selectedMarker];
+    final LatLng current = marker.position;
     final Offset offset = Offset(
       center.latitude - current.latitude,
       center.longitude - current.longitude,
     );
-    _updateSelectedMarker(
-      MarkerOptions(
-        position: LatLng(
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        positionParam: LatLng(
           center.latitude + offset.dy,
           center.longitude + offset.dx,
         ),
-      ),
-    );
+      );
+    });
   }
 
   void _changeAnchor() {
-    final Offset currentAnchor = _selectedMarker.options.anchor;
+    final Marker marker = markers[selectedMarker];
+    final Offset currentAnchor = marker.anchor;
     final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx);
-    _updateSelectedMarker(MarkerOptions(anchor: newAnchor));
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        anchorParam: newAnchor,
+      );
+    });
   }
 
   Future<void> _changeInfoAnchor() async {
-    final Offset currentAnchor = _selectedMarker.options.infoWindowAnchor;
+    final Marker marker = markers[selectedMarker];
+    final Offset currentAnchor = marker.infoWindow.anchor;
     final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx);
-    _updateSelectedMarker(MarkerOptions(infoWindowAnchor: newAnchor));
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        infoWindowParam: marker.infoWindow.copyWith(
+          anchorParam: newAnchor,
+        ),
+      );
+    });
   }
 
   Future<void> _toggleDraggable() async {
-    _updateSelectedMarker(
-      MarkerOptions(draggable: !_selectedMarker.options.draggable),
-    );
+    final Marker marker = markers[selectedMarker];
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        draggableParam: !marker.draggable,
+      );
+    });
   }
 
   Future<void> _toggleFlat() async {
-    _updateSelectedMarker(MarkerOptions(flat: !_selectedMarker.options.flat));
+    final Marker marker = markers[selectedMarker];
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        flatParam: !marker.flat,
+      );
+    });
   }
 
   Future<void> _changeInfo() async {
-    final InfoWindowText currentInfo = _selectedMarker.options.infoWindowText;
-    _updateSelectedMarker(MarkerOptions(
-      infoWindowText: InfoWindowText(
-        currentInfo.title,
-        currentInfo.snippet + '*',
-      ),
-    ));
+    final Marker marker = markers[selectedMarker];
+    final String newSnippet = marker.infoWindow.snippet + '*';
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        infoWindowParam: marker.infoWindow.copyWith(
+          snippetParam: newSnippet,
+        ),
+      );
+    });
   }
 
   Future<void> _changeAlpha() async {
-    final double current = _selectedMarker.options.alpha;
-    _updateSelectedMarker(
-      MarkerOptions(alpha: current < 0.1 ? 1.0 : current * 0.75),
-    );
+    final Marker marker = markers[selectedMarker];
+    final double current = marker.alpha;
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        alphaParam: current < 0.1 ? 1.0 : current * 0.75,
+      );
+    });
   }
 
   Future<void> _changeRotation() async {
-    final double current = _selectedMarker.options.rotation;
-    _updateSelectedMarker(
-      MarkerOptions(rotation: current == 330.0 ? 0.0 : current + 30.0),
-    );
+    final Marker marker = markers[selectedMarker];
+    final double current = marker.rotation;
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        rotationParam: current == 330.0 ? 0.0 : current + 30.0,
+      );
+    });
   }
 
   Future<void> _toggleVisible() async {
-    _updateSelectedMarker(
-      MarkerOptions(visible: !_selectedMarker.options.visible),
-    );
+    final Marker marker = markers[selectedMarker];
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        visibleParam: !marker.visible,
+      );
+    });
   }
 
   Future<void> _changeZIndex() async {
-    final double current = _selectedMarker.options.zIndex;
-    _updateSelectedMarker(
-      MarkerOptions(zIndex: current == 12.0 ? 0.0 : current + 1.0),
-    );
+    final Marker marker = markers[selectedMarker];
+    final double current = marker.zIndex;
+    setState(() {
+      markers[selectedMarker] = marker.copyWith(
+        zIndexParam: current == 12.0 ? 0.0 : current + 1.0,
+      );
+    });
   }
 
   @override
@@ -179,6 +228,10 @@
                 target: LatLng(-33.852, 151.211),
                 zoom: 11.0,
               ),
+              // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+              // https://github.com/flutter/flutter/issues/28312
+              // ignore: prefer_collection_literals
+              markers: Set<Marker>.of(markers.values),
             ),
           ),
         ),
@@ -193,22 +246,19 @@
                       children: <Widget>[
                         FlatButton(
                           child: const Text('add'),
-                          onPressed: (_markerCount == 12) ? null : _add,
+                          onPressed: _add,
                         ),
                         FlatButton(
                           child: const Text('remove'),
-                          onPressed: (_selectedMarker == null) ? null : _remove,
+                          onPressed: _remove,
                         ),
                         FlatButton(
                           child: const Text('change info'),
-                          onPressed:
-                              (_selectedMarker == null) ? null : _changeInfo,
+                          onPressed: _changeInfo,
                         ),
                         FlatButton(
                           child: const Text('change info anchor'),
-                          onPressed: (_selectedMarker == null)
-                              ? null
-                              : _changeInfoAnchor,
+                          onPressed: _changeInfoAnchor,
                         ),
                       ],
                     ),
@@ -216,46 +266,35 @@
                       children: <Widget>[
                         FlatButton(
                           child: const Text('change alpha'),
-                          onPressed:
-                              (_selectedMarker == null) ? null : _changeAlpha,
+                          onPressed: _changeAlpha,
                         ),
                         FlatButton(
                           child: const Text('change anchor'),
-                          onPressed:
-                              (_selectedMarker == null) ? null : _changeAnchor,
+                          onPressed: _changeAnchor,
                         ),
                         FlatButton(
                           child: const Text('toggle draggable'),
-                          onPressed: (_selectedMarker == null)
-                              ? null
-                              : _toggleDraggable,
+                          onPressed: _toggleDraggable,
                         ),
                         FlatButton(
                           child: const Text('toggle flat'),
-                          onPressed:
-                              (_selectedMarker == null) ? null : _toggleFlat,
+                          onPressed: _toggleFlat,
                         ),
                         FlatButton(
                           child: const Text('change position'),
-                          onPressed: (_selectedMarker == null)
-                              ? null
-                              : _changePosition,
+                          onPressed: _changePosition,
                         ),
                         FlatButton(
                           child: const Text('change rotation'),
-                          onPressed: (_selectedMarker == null)
-                              ? null
-                              : _changeRotation,
+                          onPressed: _changeRotation,
                         ),
                         FlatButton(
                           child: const Text('toggle visible'),
-                          onPressed:
-                              (_selectedMarker == null) ? null : _toggleVisible,
+                          onPressed: _toggleVisible,
                         ),
                         FlatButton(
                           child: const Text('change zIndex'),
-                          onPressed:
-                              (_selectedMarker == null) ? null : _changeZIndex,
+                          onPressed: _changeZIndex,
                         ),
                       ],
                     ),
diff --git a/packages/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/example/lib/scrolling_map.dart
index fd1661f..9597e46 100644
--- a/packages/google_maps_flutter/example/lib/scrolling_map.dart
+++ b/packages/google_maps_flutter/example/lib/scrolling_map.dart
@@ -42,14 +42,13 @@
                     width: 300.0,
                     height: 300.0,
                     child: GoogleMap(
-                      onMapCreated: onMapCreated,
                       initialCameraPosition: CameraPosition(
                         target: center,
                         zoom: 11.0,
                       ),
                       gestureRecognizers:
-                          // TODO(mklim): Remove this when collection literals
-                          // makes it to stable.
+                          // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+                          // https://github.com/flutter/flutter/issues/28312
                           // ignore: prefer_collection_literals
                           <Factory<OneSequenceGestureRecognizer>>[
                         Factory<OneSequenceGestureRecognizer>(
@@ -79,14 +78,32 @@
                     width: 300.0,
                     height: 300.0,
                     child: GoogleMap(
-                      onMapCreated: onMapCreated,
                       initialCameraPosition: CameraPosition(
                         target: center,
                         zoom: 11.0,
                       ),
+                      markers:
+                          // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+                          // https://github.com/flutter/flutter/issues/28312
+                          // ignore: prefer_collection_literals
+                          Set<Marker>.of(
+                        <Marker>[
+                          Marker(
+                            markerId: MarkerId("test_marker_id"),
+                            position: LatLng(
+                              center.latitude,
+                              center.longitude,
+                            ),
+                            infoWindow: const InfoWindow(
+                              title: 'An interesting location',
+                              snippet: '*',
+                            ),
+                          )
+                        ],
+                      ),
                       gestureRecognizers:
-                          // TODO(mklim): Remove this when collection literals
-                          // makes it to stable.
+                          // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+                          // https://github.com/flutter/flutter/issues/28312
                           // ignore: prefer_collection_literals
                           <Factory<OneSequenceGestureRecognizer>>[
                         Factory<OneSequenceGestureRecognizer>(
@@ -103,14 +120,4 @@
       ],
     );
   }
-
-  void onMapCreated(GoogleMapController controller) {
-    controller.addMarker(MarkerOptions(
-      position: LatLng(
-        center.latitude,
-        center.longitude,
-      ),
-      infoWindowText: const InfoWindowText('An interesting location', '*'),
-    ));
-  }
 }
diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart
index 3d9e185..13e2a98 100644
--- a/packages/google_maps_flutter/lib/google_maps_flutter.dart
+++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart
@@ -18,5 +18,6 @@
 part 'src/controller.dart';
 part 'src/google_map.dart';
 part 'src/marker.dart';
+part 'src/marker_updates.dart';
 part 'src/location.dart';
 part 'src/ui.dart';
diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart
index e2ff8e5..ecfc4d8 100644
--- a/packages/google_maps_flutter/lib/src/controller.dart
+++ b/packages/google_maps_flutter/lib/src/controller.dart
@@ -9,25 +9,26 @@
 /// Change listeners are notified upon changes to any of
 ///
 /// * the [options] property
-/// * the collection of [Marker]s added to this map
 /// * the [isCameraMoving] property
 /// * the [cameraPosition] property
 ///
 /// Listeners are notified after 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, MethodChannel channel, CameraPosition initialCameraPosition)
-      : assert(_id != null),
-        assert(channel != null),
+    MethodChannel channel,
+    CameraPosition initialCameraPosition,
+    this._googleMapState,
+  )   : assert(channel != null),
         _channel = channel {
     _cameraPosition = initialCameraPosition;
     _channel.setMethodCallHandler(_handleMethodCall);
   }
 
   static Future<GoogleMapController> init(
-      int id, CameraPosition initialCameraPosition) async {
+    int id,
+    CameraPosition initialCameraPosition,
+    _GoogleMapState googleMapState,
+  ) async {
     assert(id != null);
     final MethodChannel channel =
         MethodChannel('plugins.flutter.io/google_maps_$id');
@@ -35,52 +36,28 @@
     // https://github.com/flutter/flutter/issues/26431
     // ignore: strong_mode_implicit_dynamic_method
     await channel.invokeMethod('map#waitForMap');
-    return GoogleMapController._(id, channel, initialCameraPosition);
+    return GoogleMapController._(
+      channel,
+      initialCameraPosition,
+      googleMapState,
+    );
   }
 
   final MethodChannel _channel;
 
-  /// Callbacks to receive tap events for markers placed on this map.
-  final ArgumentCallbacks<Marker> onMarkerTapped = ArgumentCallbacks<Marker>();
-
-  /// Callbacks to receive tap events for info windows on markers
-  final ArgumentCallbacks<Marker> onInfoWindowTapped =
-      ArgumentCallbacks<Marker>();
-
-  /// The current set of markers on this map.
-  ///
-  /// The returned set will be a detached snapshot of the markers collection.
-  Set<Marker> get markers => Set<Marker>.from(_markers.values);
-  final Map<String, Marker> _markers = <String, Marker>{};
-
   /// True if the map camera is currently moving.
   bool get isCameraMoving => _isCameraMoving;
   bool _isCameraMoving = false;
 
+  final _GoogleMapState _googleMapState;
+
   /// Returns the most recent camera position reported by the platform side.
   /// Will be null, if [GoogleMap.trackCameraPosition] is false.
   CameraPosition get cameraPosition => _cameraPosition;
   CameraPosition _cameraPosition;
 
-  final int _id;
-
   Future<dynamic> _handleMethodCall(MethodCall call) async {
     switch (call.method) {
-      case 'infoWindow#onTap':
-        final String markerId = call.arguments['marker'];
-        final Marker marker = _markers[markerId];
-        if (marker != null) {
-          onInfoWindowTapped(marker);
-        }
-        break;
-
-      case 'marker#onTap':
-        final String markerId = call.arguments['marker'];
-        final Marker marker = _markers[markerId];
-        if (marker != null) {
-          onMarkerTapped(marker);
-        }
-        break;
       case 'camera#onMoveStarted':
         _isCameraMoving = true;
         notifyListeners();
@@ -93,6 +70,12 @@
         _isCameraMoving = false;
         notifyListeners();
         break;
+      case 'marker#onTap':
+        _googleMapState.onMarkerTap(call.arguments['markerId']);
+        break;
+      case 'infoWindow#onTap':
+        _googleMapState.onInfoWindowTap(call.arguments['markerId']);
+        break;
       default:
         throw MissingPluginException();
     }
@@ -119,6 +102,24 @@
     notifyListeners();
   }
 
+  /// Updates marker configuration.
+  ///
+  /// Change listeners are notified once the update has been made on the
+  /// platform side.
+  ///
+  /// The returned [Future] completes after listeners have been notified.
+  Future<void> _updateMarkers(_MarkerUpdates markerUpdates) async {
+    assert(markerUpdates != null);
+    // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
+    // https://github.com/flutter/flutter/issues/26431
+    // ignore: strong_mode_implicit_dynamic_method
+    await _channel.invokeMethod(
+      'markers#update',
+      markerUpdates._toMap(),
+    );
+    notifyListeners();
+  }
+
   /// Starts an animated change of the map camera position.
   ///
   /// The returned [Future] completes after the change has been started on the
@@ -144,95 +145,4 @@
       'cameraUpdate': cameraUpdate._toJson(),
     });
   }
-
-  /// Adds a marker to the map, configured using the specified custom [options].
-  ///
-  /// Change listeners are notified once the marker has been added on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes with the added marker once listeners have
-  /// been notified.
-  Future<Marker> addMarker(MarkerOptions options) async {
-    final MarkerOptions effectiveOptions =
-        MarkerOptions.defaultOptions.copyWith(options);
-    // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
-    // https://github.com/flutter/flutter/issues/26431
-    // ignore: strong_mode_implicit_dynamic_method
-    final String markerId = await _channel.invokeMethod(
-      'marker#add',
-      <String, dynamic>{
-        'options': effectiveOptions._toJson(),
-      },
-    );
-    final Marker marker = Marker(markerId, effectiveOptions);
-    _markers[markerId] = marker;
-    notifyListeners();
-    return marker;
-  }
-
-  /// Updates the specified [marker] with the given [changes]. The marker must
-  /// be a current member of the [markers] set.
-  ///
-  /// Change listeners are notified once the marker has been updated on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes once listeners have been notified.
-  Future<void> updateMarker(Marker marker, MarkerOptions changes) async {
-    assert(marker != null);
-    assert(_markers[marker._id] == marker);
-    assert(changes != null);
-    // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
-    // https://github.com/flutter/flutter/issues/26431
-    // ignore: strong_mode_implicit_dynamic_method
-    await _channel.invokeMethod('marker#update', <String, dynamic>{
-      'marker': marker._id,
-      'options': changes._toJson(),
-    });
-    marker._options = marker._options.copyWith(changes);
-    notifyListeners();
-  }
-
-  /// Removes the specified [marker] from the map. The marker must be a current
-  /// member of the [markers] set.
-  ///
-  /// Change listeners are notified once the marker has been removed on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes once listeners have been notified.
-  Future<void> removeMarker(Marker marker) async {
-    assert(marker != null);
-    assert(_markers[marker._id] == marker);
-    await _removeMarker(marker._id);
-    notifyListeners();
-  }
-
-  /// Removes all [markers] from the map.
-  ///
-  /// Change listeners are notified once all markers have been removed on the
-  /// platform side.
-  ///
-  /// The returned [Future] completes once listeners have been notified.
-  Future<void> clearMarkers() async {
-    assert(_markers != null);
-    final List<String> markerIds = List<String>.from(_markers.keys);
-    for (String id in markerIds) {
-      await _removeMarker(id);
-    }
-    notifyListeners();
-  }
-
-  /// Helper method to remove a single marker from the map. Consumed by
-  /// [removeMarker] and [clearMarkers].
-  ///
-  /// The returned [Future] completes once the marker has been removed from
-  /// [_markers].
-  Future<void> _removeMarker(String id) async {
-    // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
-    // https://github.com/flutter/flutter/issues/26431
-    // ignore: strong_mode_implicit_dynamic_method
-    await _channel.invokeMethod('marker#remove', <String, dynamic>{
-      'marker': id,
-    });
-    _markers.remove(id);
-  }
 }
diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart
index a1f1ad4..365ac0c 100644
--- a/packages/google_maps_flutter/lib/src/google_map.dart
+++ b/packages/google_maps_flutter/lib/src/google_map.dart
@@ -21,6 +21,7 @@
     this.tiltGesturesEnabled = true,
     this.trackCameraPosition = false,
     this.myLocationEnabled = false,
+    this.markers,
   }) : assert(initialCameraPosition != null);
 
   final MapCreatedCallback onMapCreated;
@@ -57,6 +58,9 @@
   /// True if the map view should relay camera move events to Flutter.
   final bool trackCameraPosition;
 
+  // Markers to be placed on the map.
+  final Set<Marker> markers;
+
   /// True if a "My Location" layer should be shown on the map.
   ///
   /// This layer includes a location indicator at the current device location,
@@ -101,13 +105,15 @@
   final Completer<GoogleMapController> _controller =
       Completer<GoogleMapController>();
 
+  Map<MarkerId, Marker> _markers = <MarkerId, Marker>{};
   _GoogleMapOptions _googleMapOptions;
 
   @override
   Widget build(BuildContext context) {
     final Map<String, dynamic> creationParams = <String, dynamic>{
       'initialCameraPosition': widget.initialCameraPosition?._toMap(),
-      'options': _GoogleMapOptions.fromWidget(widget).toMap(),
+      'options': _googleMapOptions.toMap(),
+      'markersToAdd': _serializeMarkerSet(widget.markers),
     };
     if (defaultTargetPlatform == TargetPlatform.android) {
       return AndroidView(
@@ -135,34 +141,58 @@
   void initState() {
     super.initState();
     _googleMapOptions = _GoogleMapOptions.fromWidget(widget);
+    _markers = _keyByMarkerId(widget.markers);
   }
 
   @override
   void didUpdateWidget(GoogleMap oldWidget) {
     super.didUpdateWidget(oldWidget);
+    _updateOptions();
+    _updateMarkers();
+  }
+
+  void _updateOptions() async {
     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);
+    _googleMapOptions = newOptions;
+  }
+
+  void _updateMarkers() async {
+    final GoogleMapController controller = await _controller.future;
+    controller._updateMarkers(
+        _MarkerUpdates.from(_markers.values.toSet(), widget.markers));
+    _markers = _keyByMarkerId(widget.markers);
   }
 
   Future<void> onPlatformViewCreated(int id) async {
-    final GoogleMapController controller =
-        await GoogleMapController.init(id, widget.initialCameraPosition);
+    final GoogleMapController controller = await GoogleMapController.init(
+      id,
+      widget.initialCameraPosition,
+      this,
+    );
     _controller.complete(controller);
     if (widget.onMapCreated != null) {
       widget.onMapCreated(controller);
     }
   }
+
+  void onMarkerTap(String markerIdParam) {
+    assert(markerIdParam != null);
+    final MarkerId markerId = MarkerId(markerIdParam);
+    _markers[markerId].onTap();
+  }
+
+  void onInfoWindowTap(String markerIdParam) {
+    assert(markerIdParam != null);
+    final MarkerId markerId = MarkerId(markerIdParam);
+    _markers[markerId].infoWindow.onTap();
+  }
 }
 
 /// Configuration options for the GoogleMaps user interface.
@@ -237,11 +267,13 @@
     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/marker.dart b/packages/google_maps_flutter/lib/src/marker.dart
index 3521497..cd39f68 100644
--- a/packages/google_maps_flutter/lib/src/marker.dart
+++ b/packages/google_maps_flutter/lib/src/marker.dart
@@ -4,33 +4,6 @@
 
 part of google_maps_flutter;
 
-/// An icon placed at a particular geographical location 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 events
-/// as markers are added, updated, tapped, and removed.
-class Marker {
-  @visibleForTesting
-  Marker(this._id, this._options);
-
-  /// A unique identifier for this marker.
-  ///
-  /// The identirifer is an arbitrary unique string.
-  final String _id;
-  String get id => _id;
-
-  MarkerOptions _options;
-
-  /// The marker configuration options most recently applied programmatically
-  /// via the map controller.
-  ///
-  /// The returned value does not reflect any changes made to the marker through
-  /// touch events. Add listeners to the owning map controller to track those.
-  MarkerOptions get options => _options;
-}
-
 dynamic _offsetToJson(Offset offset) {
   if (offset == null) {
     return null;
@@ -39,11 +12,16 @@
 }
 
 /// Text labels for a [Marker] info window.
-class InfoWindowText {
-  const InfoWindowText(this.title, this.snippet);
+class InfoWindow {
+  const InfoWindow({
+    this.title,
+    this.snippet,
+    this.anchor = const Offset(0.5, 0.0),
+    this.onTap,
+  });
 
   /// Text labels specifying that no text is to be displayed.
-  static const InfoWindowText noText = InfoWindowText(null, null);
+  static const InfoWindow noText = InfoWindow();
 
   /// Text displayed in an info window when the user taps the marker.
   ///
@@ -55,33 +33,138 @@
   /// A null value means no additional text.
   final String snippet;
 
-  dynamic _toJson() => <dynamic>[title, snippet];
+  /// The icon image point that will be the anchor of the info window when
+  /// displayed.
+  ///
+  /// The image point is specified in normalized coordinates: An anchor of
+  /// (0.0, 0.0) means the top left corner of the image. An anchor
+  /// of (1.0, 1.0) means the bottom right corner of the image.
+  final Offset anchor;
+
+  /// onTap callback for this [InfoWindow].
+  final VoidCallback onTap;
+
+  /// Creates a new [InfoWindow] object whose values are the same as this instance,
+  /// unless overwritten by the specified parameters.
+  InfoWindow copyWith({
+    String titleParam,
+    String snippetParam,
+    Offset anchorParam,
+    VoidCallback onTapParam,
+  }) {
+    return InfoWindow(
+      title: titleParam ?? title,
+      snippet: snippetParam ?? snippet,
+      anchor: anchorParam ?? anchor,
+      onTap: onTapParam ?? onTap,
+    );
+  }
+
+  dynamic _toJson() {
+    final Map<String, dynamic> json = <String, dynamic>{};
+
+    void addIfPresent(String fieldName, dynamic value) {
+      if (value != null) {
+        json[fieldName] = value;
+      }
+    }
+
+    addIfPresent('title', title);
+    addIfPresent('snippet', snippet);
+    addIfPresent('anchor', _offsetToJson(anchor));
+
+    return json;
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    if (other.runtimeType != runtimeType) return false;
+    final InfoWindow typedOther = other;
+    return title == typedOther.title &&
+        snippet == typedOther.snippet &&
+        anchor == typedOther.anchor;
+  }
+
+  @override
+  int get hashCode => hashValues(title.hashCode, snippet, anchor);
+
+  @override
+  String toString() {
+    return 'InfoWindow{title: $title, snippet: $snippet, anchor: $anchor}';
+  }
 }
 
-/// Configuration options for [Marker] instances.
+/// Uniquely identifies a [Marker] among [GoogleMap] markers.
 ///
-/// When used to change configuration, null values will be interpreted as
-/// "do not change this configuration option".
-class MarkerOptions {
+/// This does not have to be globally unique, only unique among the list.
+@immutable
+class MarkerId {
+  MarkerId(this.value) : assert(value != null);
+
+  /// value of the [MarkerId].
+  final String value;
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    if (other.runtimeType != runtimeType) return false;
+    final MarkerId typedOther = other;
+    return value == typedOther.value;
+  }
+
+  @override
+  int get hashCode => value.hashCode;
+
+  @override
+  String toString() {
+    return 'MarkerId{value: $value}';
+  }
+}
+
+/// Marks a geographical location on the map.
+///
+/// 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.
+@immutable
+class Marker {
   /// Creates a set of marker configuration options.
   ///
-  /// By default, every non-specified field is null, meaning no desire to change
-  /// marker defaults or current configuration.
-  const MarkerOptions({
-    this.alpha,
-    this.anchor,
-    this.consumeTapEvents,
-    this.draggable,
-    this.flat,
-    this.icon,
-    this.infoWindowAnchor,
-    this.infoWindowText,
-    this.position,
-    this.rotation,
-    this.visible,
-    this.zIndex,
+  /// Default marker options.
+  ///
+  /// Specifies a marker that
+  /// * is fully opaque; [alpha] is 1.0
+  /// * uses icon bottom center to indicate map position; [anchor] is (0.5, 1.0)
+  /// * has default tap handling; [consumeTapEvents] is false
+  /// * is stationary; [draggable] is false
+  /// * is drawn against the screen, not the map; [flat] is false
+  /// * has a default icon; [icon] is `BitmapDescriptor.defaultMarker`
+  /// * anchors the info window at top center; [infoWindowAnchor] is (0.5, 0.0)
+  /// * has no info window text; [infoWindowText] is `InfoWindowText.noText`
+  /// * is positioned at 0, 0; [position] is `LatLng(0.0, 0.0)`
+  /// * has an axis-aligned icon; [rotation] is 0.0
+  /// * is visible; [visible] is true
+  /// * is placed at the base of the drawing order; [zIndex] is 0.0
+  const Marker({
+    @required this.markerId,
+    this.alpha = 1.0,
+    this.anchor = const Offset(0.5, 1.0),
+    this.consumeTapEvents = false,
+    this.draggable = false,
+    this.flat = false,
+    this.icon = BitmapDescriptor.defaultMarker,
+    this.infoWindow = InfoWindow.noText,
+    this.position = const LatLng(0.0, 0.0),
+    this.rotation = 0.0,
+    this.visible = true,
+    this.zIndex = 0.0,
+    this.onTap,
   }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0));
 
+  /// Uniquely identifies a [Marker].
+  final MarkerId markerId;
+
   /// The opacity of the marker, between 0.0 and 1.0 inclusive.
   ///
   /// 0.0 means fully transparent, 1.0 means fully opaque.
@@ -109,16 +192,10 @@
   /// A description of the bitmap used to draw the marker icon.
   final BitmapDescriptor icon;
 
-  /// The icon image point that will be the anchor of the info window when
-  /// displayed.
+  /// A Google Maps InfoWindow.
   ///
-  /// The image point is specified in normalized coordinates: An anchor of
-  /// (0.0, 0.0) means the top left corner of the image. An anchor
-  /// of (1.0, 1.0) means the bottom right corner of the image.
-  final Offset infoWindowAnchor;
-
-  /// Text content for the info window.
-  final InfoWindowText infoWindowText;
+  /// The window is displayed when the marker is tapped.
+  final InfoWindow infoWindow;
 
   /// Geographical location of the marker.
   final LatLng position;
@@ -136,61 +213,43 @@
   /// earlier, and thus appearing to be closer to the surface of the Earth.
   final double zIndex;
 
-  /// Default marker options.
-  ///
-  /// Specifies a marker that
-  /// * is fully opaque; [alpha] is 1.0
-  /// * uses icon bottom center to indicate map position; [anchor] is (0.5, 1.0)
-  /// * has default tap handling; [consumeTapEvents] is false
-  /// * is stationary; [draggable] is false
-  /// * is drawn against the screen, not the map; [flat] is false
-  /// * has a default icon; [icon] is `BitmapDescriptor.defaultMarker`
-  /// * anchors the info window at top center; [infoWindowAnchor] is (0.5, 0.0)
-  /// * has no info window text; [infoWindowText] is `InfoWindowText.noText`
-  /// * is positioned at 0, 0; [position] is `LatLng(0.0, 0.0)`
-  /// * has an axis-aligned icon; [rotation] is 0.0
-  /// * is visible; [visible] is true
-  /// * is placed at the base of the drawing order; [zIndex] is 0.0
-  static const MarkerOptions defaultOptions = MarkerOptions(
-    alpha: 1.0,
-    anchor: Offset(0.5, 1.0),
-    consumeTapEvents: false,
-    draggable: false,
-    flat: false,
-    icon: BitmapDescriptor.defaultMarker,
-    infoWindowAnchor: Offset(0.5, 0.0),
-    infoWindowText: InfoWindowText.noText,
-    position: LatLng(0.0, 0.0),
-    rotation: 0.0,
-    visible: true,
-    zIndex: 0.0,
-  );
+  /// Callbacks to receive tap events for markers placed on this map.
+  final VoidCallback onTap;
 
-  /// 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.
-  MarkerOptions copyWith(MarkerOptions changes) {
-    if (changes == null) {
-      return this;
-    }
-    return MarkerOptions(
-      alpha: changes.alpha ?? alpha,
-      anchor: changes.anchor ?? anchor,
-      consumeTapEvents: changes.consumeTapEvents ?? consumeTapEvents,
-      draggable: changes.draggable ?? draggable,
-      flat: changes.flat ?? flat,
-      icon: changes.icon ?? icon,
-      infoWindowAnchor: changes.infoWindowAnchor ?? infoWindowAnchor,
-      infoWindowText: changes.infoWindowText ?? infoWindowText,
-      position: changes.position ?? position,
-      rotation: changes.rotation ?? rotation,
-      visible: changes.visible ?? visible,
-      zIndex: changes.zIndex ?? zIndex,
+  /// Creates a new [Marker] object whose values are the same as this instance,
+  /// unless overwritten by the specified parameters.
+  Marker copyWith({
+    double alphaParam,
+    Offset anchorParam,
+    bool consumeTapEventsParam,
+    bool draggableParam,
+    bool flatParam,
+    BitmapDescriptor iconParam,
+    InfoWindow infoWindowParam,
+    LatLng positionParam,
+    double rotationParam,
+    bool visibleParam,
+    double zIndexParam,
+    VoidCallback onTapParam,
+  }) {
+    return Marker(
+      markerId: markerId,
+      alpha: alphaParam ?? alpha,
+      anchor: anchorParam ?? anchor,
+      consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents,
+      draggable: draggableParam ?? draggable,
+      flat: flatParam ?? flat,
+      icon: iconParam ?? icon,
+      infoWindow: infoWindowParam ?? infoWindow,
+      position: positionParam ?? position,
+      rotation: rotationParam ?? rotation,
+      visible: visibleParam ?? visible,
+      zIndex: zIndexParam ?? zIndex,
+      onTap: onTapParam ?? onTap,
     );
   }
 
-  dynamic _toJson() {
+  Map<String, dynamic> _toJson() {
     final Map<String, dynamic> json = <String, dynamic>{};
 
     void addIfPresent(String fieldName, dynamic value) {
@@ -199,18 +258,52 @@
       }
     }
 
+    addIfPresent('markerId', markerId.value);
     addIfPresent('alpha', alpha);
     addIfPresent('anchor', _offsetToJson(anchor));
     addIfPresent('consumeTapEvents', consumeTapEvents);
     addIfPresent('draggable', draggable);
     addIfPresent('flat', flat);
     addIfPresent('icon', icon?._toJson());
-    addIfPresent('infoWindowAnchor', _offsetToJson(infoWindowAnchor));
-    addIfPresent('infoWindowText', infoWindowText?._toJson());
+    addIfPresent('infoWindow', infoWindow?._toJson());
     addIfPresent('position', position?._toJson());
     addIfPresent('rotation', rotation);
     addIfPresent('visible', visible);
     addIfPresent('zIndex', zIndex);
     return json;
   }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    if (other.runtimeType != runtimeType) return false;
+    final Marker typedOther = other;
+    return markerId == typedOther.markerId;
+  }
+
+  @override
+  int get hashCode => markerId.hashCode;
+
+  @override
+  String toString() {
+    return 'Marker{markerId: $markerId, alpha: $alpha, anchor: $anchor, '
+        'consumeTapEvents: $consumeTapEvents, draggable: $draggable, flat: $flat, '
+        'icon: $icon, infoWindow: $infoWindow, position: $position, rotation: $rotation, '
+        'visible: $visible, zIndex: $zIndex, onTap: $onTap}';
+  }
+}
+
+Map<MarkerId, Marker> _keyByMarkerId(Iterable<Marker> markers) {
+  if (markers == null) {
+    return <MarkerId, Marker>{};
+  }
+  return Map<MarkerId, Marker>.fromEntries(markers.map(
+      (Marker marker) => MapEntry<MarkerId, Marker>(marker.markerId, marker)));
+}
+
+List<Map<String, dynamic>> _serializeMarkerSet(Set<Marker> markers) {
+  if (markers == null) {
+    return null;
+  }
+  return markers.map<Map<String, dynamic>>((Marker m) => m._toJson()).toList();
 }
diff --git a/packages/google_maps_flutter/lib/src/marker_updates.dart b/packages/google_maps_flutter/lib/src/marker_updates.dart
new file mode 100644
index 0000000..6c73b5e
--- /dev/null
+++ b/packages/google_maps_flutter/lib/src/marker_updates.dart
@@ -0,0 +1,90 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+part of google_maps_flutter;
+
+/// [Marker] update events to be applied to the [GoogleMap].
+///
+/// Used in [GoogleMapController] when the map is updated.
+class _MarkerUpdates {
+  /// Computes [_MarkerUpdates] given previous and current [Marker]s.
+  _MarkerUpdates.from(Set<Marker> previous, Set<Marker> current) {
+    if (previous == null) {
+      previous = Set<Marker>.identity();
+    }
+
+    if (current == null) {
+      current = Set<Marker>.identity();
+    }
+
+    final Map<MarkerId, Marker> previousMarkers = _keyByMarkerId(previous);
+    final Map<MarkerId, Marker> currentMarkers = _keyByMarkerId(current);
+
+    final Set<MarkerId> prevMarkerIds = previousMarkers.keys.toSet();
+    final Set<MarkerId> currentMarkerIds = currentMarkers.keys.toSet();
+
+    Marker idToCurrentMarker(MarkerId id) {
+      return currentMarkers[id];
+    }
+
+    final Set<MarkerId> _markerIdsToRemove =
+        prevMarkerIds.difference(currentMarkerIds);
+
+    final Set<Marker> _markersToAdd = currentMarkerIds
+        .difference(prevMarkerIds)
+        .map(idToCurrentMarker)
+        .toSet();
+
+    final Set<Marker> _markersToChange = currentMarkerIds
+        .intersection(prevMarkerIds)
+        .map(idToCurrentMarker)
+        .toSet();
+
+    markersToAdd = _markersToAdd;
+    markerIdsToRemove = _markerIdsToRemove;
+    markersToChange = _markersToChange;
+  }
+
+  Set<Marker> markersToAdd;
+  Set<MarkerId> markerIdsToRemove;
+  Set<Marker> markersToChange;
+
+  Map<String, dynamic> _toMap() {
+    final Map<String, dynamic> updateMap = <String, dynamic>{};
+
+    void addIfNonNull(String fieldName, dynamic value) {
+      if (value != null) {
+        updateMap[fieldName] = value;
+      }
+    }
+
+    addIfNonNull('markersToAdd', _serializeMarkerSet(markersToAdd));
+    addIfNonNull('markersToChange', _serializeMarkerSet(markersToChange));
+    addIfNonNull('markerIdsToRemove',
+        markerIdsToRemove.map<dynamic>((MarkerId m) => m.value).toList());
+
+    return updateMap;
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    if (other.runtimeType != runtimeType) return false;
+    final _MarkerUpdates typedOther = other;
+    return setEquals(markersToAdd, typedOther.markersToAdd) &&
+        setEquals(markerIdsToRemove, typedOther.markerIdsToRemove) &&
+        setEquals(markersToChange, typedOther.markersToChange);
+  }
+
+  @override
+  int get hashCode =>
+      hashValues(markersToAdd, markerIdsToRemove, markersToChange);
+
+  @override
+  String toString() {
+    return '_MarkerUpdates{markersToAdd: $markersToAdd, '
+        'markerIdsToRemove: $markerIdsToRemove, '
+        'markersToChange: $markersToChange}';
+  }
+}
diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml
index f6b5023..d7ee759 100644
--- a/packages/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
 author: Flutter Team <flutter-dev@googlegroups.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter
-version: 0.2.0+6
+version: 0.3.0
 
 dependencies:
   flutter:
diff --git a/packages/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/test/fake_maps_controllers.dart
new file mode 100644
index 0000000..fb7308c
--- /dev/null
+++ b/packages/google_maps_flutter/test/fake_maps_controllers.dart
@@ -0,0 +1,191 @@
+// 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_test/flutter_test.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+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']);
+    updateMarkers(params);
+  }
+
+  MethodChannel channel;
+
+  CameraPosition cameraPosition;
+
+  bool compassEnabled;
+
+  CameraTargetBounds cameraTargetBounds;
+
+  MapType mapType;
+
+  MinMaxZoomPreference minMaxZoomPreference;
+
+  bool rotateGesturesEnabled;
+
+  bool scrollGesturesEnabled;
+
+  bool tiltGesturesEnabled;
+
+  bool zoomGesturesEnabled;
+
+  bool trackCameraPosition;
+
+  bool myLocationEnabled;
+
+  Set<MarkerId> markerIdsToRemove;
+
+  Set<Marker> markersToAdd;
+
+  Set<Marker> markersToChange;
+
+  Future<dynamic> onMethodCall(MethodCall call) {
+    switch (call.method) {
+      case 'map#update':
+        updateOptions(call.arguments['options']);
+        return Future<void>.sync(() {});
+      case 'markers#update':
+        updateMarkers(call.arguments);
+        return Future<void>.sync(() {});
+      default:
+        return Future<void>.sync(() {});
+    }
+  }
+
+  void updateMarkers(Map<dynamic, dynamic> markerUpdates) {
+    if (markerUpdates == null) {
+      return;
+    }
+    markersToAdd = _deserializeMarkers(markerUpdates['markersToAdd']);
+    markerIdsToRemove =
+        _deserializeMarkerIds(markerUpdates['markerIdsToRemove']);
+    markersToChange = _deserializeMarkers(markerUpdates['markersToChange']);
+  }
+
+  Set<MarkerId> _deserializeMarkerIds(List<dynamic> markerIds) {
+    if (markerIds == null) {
+      // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+      // https://github.com/flutter/flutter/issues/28312
+      // ignore: prefer_collection_literals
+      return Set<MarkerId>();
+    }
+    return markerIds.map((dynamic markerId) => MarkerId(markerId)).toSet();
+  }
+
+  Set<Marker> _deserializeMarkers(dynamic markers) {
+    if (markers == null) {
+      // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+      // https://github.com/flutter/flutter/issues/28312
+      // ignore: prefer_collection_literals
+      return Set<Marker>();
+    }
+    final List<dynamic> markersData = markers;
+    // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+    // https://github.com/flutter/flutter/issues/28312
+    // ignore: prefer_collection_literals
+    final Set<Marker> result = Set<Marker>();
+    for (Map<dynamic, dynamic> markerData in markersData) {
+      final String markerId = markerData['markerId'];
+      final bool draggable = markerData['draggable'];
+      final bool visible = markerData['visible'];
+
+      final dynamic infoWindowData = markerData['infoWindow'];
+      InfoWindow infoWindow = InfoWindow.noText;
+      if (infoWindowData != null) {
+        final Map<dynamic, dynamic> infoWindowMap = infoWindowData;
+        infoWindow = InfoWindow(
+          title: infoWindowMap['title'],
+          snippet: infoWindowMap['snippet'],
+        );
+      }
+
+      result.add(Marker(
+        markerId: MarkerId(markerId),
+        draggable: draggable,
+        visible: visible,
+        infoWindow: infoWindow,
+      ));
+    }
+
+    return result;
+  }
+
+  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);
+}
diff --git a/packages/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/test/google_map_test.dart
index 2f20a37..50c3674 100644
--- a/packages/google_maps_flutter/test/google_map_test.dart
+++ b/packages/google_maps_flutter/test/google_map_test.dart
@@ -2,16 +2,16 @@
 // 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';
 
+import 'fake_maps_controllers.dart';
+
 void main() {
-  final _FakePlatformViewsController fakePlatformViewsController =
-      _FakePlatformViewsController();
+  final FakePlatformViewsController fakePlatformViewsController =
+      FakePlatformViewsController();
 
   setUpAll(() {
     SystemChannels.platform_views.setMockMethodCallHandler(
@@ -384,116 +384,3 @@
     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);
-}
diff --git a/packages/google_maps_flutter/test/marker_updates_test.dart b/packages/google_maps_flutter/test/marker_updates_test.dart
new file mode 100644
index 0000000..e9c0990
--- /dev/null
+++ b/packages/google_maps_flutter/test/marker_updates_test.dart
@@ -0,0 +1,175 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+import 'fake_maps_controllers.dart';
+
+Set<Marker> _toSet({Marker m1, Marker m2, Marker m3}) {
+  final Set<Marker> res = Set<Marker>.identity();
+  if (m1 != null) {
+    res.add(m1);
+  }
+  if (m2 != null) {
+    res.add(m2);
+  }
+  if (m3 != null) {
+    res.add(m3);
+  }
+  return res;
+}
+
+Widget _mapWithMarkers(Set<Marker> markers) {
+  return Directionality(
+    textDirection: TextDirection.ltr,
+    child: GoogleMap(
+      initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)),
+      markers: markers,
+    ),
+  );
+}
+
+void main() {
+  final FakePlatformViewsController fakePlatformViewsController =
+      FakePlatformViewsController();
+
+  setUpAll(() {
+    SystemChannels.platform_views.setMockMethodCallHandler(
+        fakePlatformViewsController.fakePlatformViewsMethodHandler);
+  });
+
+  setUp(() {
+    fakePlatformViewsController.reset();
+  });
+
+  testWidgets('Initializing a marker', (WidgetTester tester) async {
+    final Marker m1 = Marker(markerId: MarkerId("marker_1"));
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.markersToAdd.length, 1);
+
+    final Marker initializedMarker = platformGoogleMap.markersToAdd.first;
+    expect(initializedMarker, equals(m1));
+    expect(platformGoogleMap.markerIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.markersToChange.isEmpty, true);
+  });
+
+  testWidgets("Adding a marker", (WidgetTester tester) async {
+    final Marker m1 = Marker(markerId: MarkerId("marker_1"));
+    final Marker m2 = Marker(markerId: MarkerId("marker_2"));
+
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1)));
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1, m2: m2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.markersToAdd.length, 1);
+
+    final Marker addedMarker = platformGoogleMap.markersToAdd.first;
+    expect(addedMarker, equals(m2));
+    expect(platformGoogleMap.markerIdsToRemove.isEmpty, true);
+
+    expect(platformGoogleMap.markersToChange.length, 1);
+    expect(platformGoogleMap.markersToChange.first, equals(m1));
+  });
+
+  testWidgets("Removing a marker", (WidgetTester tester) async {
+    final Marker m1 = Marker(markerId: MarkerId("marker_1"));
+
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1)));
+    await tester.pumpWidget(_mapWithMarkers(null));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.markerIdsToRemove.length, 1);
+    expect(platformGoogleMap.markerIdsToRemove.first, equals(m1.markerId));
+
+    expect(platformGoogleMap.markersToChange.isEmpty, true);
+    expect(platformGoogleMap.markersToAdd.isEmpty, true);
+  });
+
+  testWidgets("Updating a marker", (WidgetTester tester) async {
+    final Marker m1 = Marker(markerId: MarkerId("marker_1"));
+    final Marker m2 = Marker(markerId: MarkerId("marker_1"), alpha: 0.5);
+
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1)));
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.markersToChange.length, 1);
+    expect(platformGoogleMap.markersToChange.first, equals(m2));
+
+    expect(platformGoogleMap.markerIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.markersToAdd.isEmpty, true);
+  });
+
+  testWidgets("Updating a marker", (WidgetTester tester) async {
+    final Marker m1 = Marker(markerId: MarkerId("marker_1"));
+    final Marker m2 = Marker(
+      markerId: MarkerId("marker_1"),
+      infoWindow: const InfoWindow(snippet: 'changed'),
+    );
+
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1)));
+    await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m2)));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+    expect(platformGoogleMap.markersToChange.length, 1);
+
+    final Marker update = platformGoogleMap.markersToChange.first;
+    expect(update, equals(m2));
+    expect(update.infoWindow.snippet, 'changed');
+  });
+
+  testWidgets("Multi Update", (WidgetTester tester) async {
+    Marker m1 = Marker(markerId: MarkerId("marker_1"));
+    Marker m2 = Marker(markerId: MarkerId("marker_2"));
+    final Set<Marker> prev = _toSet(m1: m1, m2: m2);
+    m1 = Marker(markerId: MarkerId("marker_1"), visible: false);
+    m2 = Marker(markerId: MarkerId("marker_2"), draggable: true);
+    final Set<Marker> cur = _toSet(m1: m1, m2: m2);
+
+    await tester.pumpWidget(_mapWithMarkers(prev));
+    await tester.pumpWidget(_mapWithMarkers(cur));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.markersToChange, cur);
+    expect(platformGoogleMap.markerIdsToRemove.isEmpty, true);
+    expect(platformGoogleMap.markersToAdd.isEmpty, true);
+  });
+
+  testWidgets("Multi Update", (WidgetTester tester) async {
+    Marker m2 = Marker(markerId: MarkerId("marker_2"));
+    final Marker m3 = Marker(markerId: MarkerId("marker_3"));
+    final Set<Marker> prev = _toSet(m2: m2, m3: m3);
+
+    // m1 is added, m2 is updated, m3 is removed.
+    final Marker m1 = Marker(markerId: MarkerId("marker_1"));
+    m2 = Marker(markerId: MarkerId("marker_2"), draggable: true);
+    final Set<Marker> cur = _toSet(m1: m1, m2: m2);
+
+    await tester.pumpWidget(_mapWithMarkers(prev));
+    await tester.pumpWidget(_mapWithMarkers(cur));
+
+    final FakePlatformGoogleMap platformGoogleMap =
+        fakePlatformViewsController.lastCreatedView;
+
+    expect(platformGoogleMap.markersToChange.length, 1);
+    expect(platformGoogleMap.markersToAdd.length, 1);
+    expect(platformGoogleMap.markerIdsToRemove.length, 1);
+
+    expect(platformGoogleMap.markersToChange.first, equals(m2));
+    expect(platformGoogleMap.markersToAdd.first, equals(m1));
+    expect(platformGoogleMap.markerIdsToRemove.first, equals(m3.markerId));
+  });
+}