[google_maps_flutter] Marker APIs are now widget based (Dart Changes)#1239
[google_maps_flutter] Marker APIs are now widget based (Dart Changes)#1239iskakaushik merged 23 commits intoflutter:masterfrom iskakaushik:feature/marker-api-mod-dart
Conversation
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.
| ), | ||
| gestureRecognizers: | ||
| <Factory<OneSequenceGestureRecognizer>>[ | ||
| markers: { |
There was a problem hiding this comment.
discussed offline but leaving as a reminder - we can't use set literals here yet.
| void _updateOptions(Map<String, dynamic> updates) async { | ||
| void _updateOptions( | ||
| Map<String, dynamic> updates, MarkerUpdates markerUpdates) async { | ||
| if (updates.isEmpty) { |
There was a problem hiding this comment.
You might want to update the markers even if there is no options update.
Might be clearer to just split the markers update to an _updateMarkers method.
| } | ||
| final GoogleMapController controller = await _controller.future; | ||
| controller._updateMapOptions(updates); | ||
| final MarkerControllers markerControllers = await _markerControllers.future; |
| (String key, dynamic value) => prevOptionsMap[key] == value); | ||
| ..remove('markers') | ||
| ..removeWhere((String key, dynamic value) => prevOptionsMap[key] == value) | ||
| ..putIfAbsent('markerUpdates', () => markerUpdates._toJson()); |
There was a problem hiding this comment.
Took me a little bit to wrap my ahead around who's responsible for the marker updates.
It used to be that _GoogleMapOptions is holds all the options data, this data is passed in the creation parameters 'options' field, _GoogleMapOptions knows to compute update deltas which are passed to _updateMapOptions.
The markers field doesn't follow this same pattern: it is still passed as part of the options in the creation parameters, but _GoogleMapOptions is not responsible for computing updates for it, and _updateMapOptions ignores it.
What do you think about keeping a _markers field in _GoogleMapState?
| marker._options = marker._options.copyWith(changes); | ||
| notifyListeners(); | ||
| /// Handles callbacks for events on [Marker] and [InfoWindow]. | ||
| class MarkerController { |
There was a problem hiding this comment.
Making this part of the public API limits are flexibility with breaking changes here, we better keep this and MarkersController private API if we can.
| await _removeMarker(marker._id); | ||
| notifyListeners(); | ||
| final String id = marker.markerId.value; | ||
| // TODO (kaushik) using id in the plugin handle might not be a good idea. |
There was a problem hiding this comment.
nit: should be TODO(iskakaushik): ... (here and elsewhere)
| marker._options = marker._options.copyWith(changes); | ||
| notifyListeners(); | ||
| /// Handles callbacks for events on [Marker] and [InfoWindow]. | ||
| class MarkerController { |
There was a problem hiding this comment.
The name MarkerController hints to me that it controls everything related to this marker (e.g icon, position, etc...) this is more of a MarkerEventDelegate
| <MarkerId, MarkerController>{}; | ||
|
|
||
| void update(MarkerUpdates markerUpdates) { | ||
| markerUpdates.markerUpdates.forEach((MarkerUpdate markerUpdate) { |
There was a problem hiding this comment.
I think the code would be simpler if we would just use the map method channel for all markers, that way we could save the management of the controllers here and e.g when we get a marker#onTap(markerId) all we need to do is fetch the Marker with id markerId from the markers set we maintain anyway and invoke the callback.
| } | ||
|
|
||
| static Map<MarkerId, Marker> _createMarkerIdMarkerMap(Set<Marker> markers) { | ||
| // TODO(iskakaushik): Remove this when collection literals makes it to stable. |
There was a problem hiding this comment.
An alternative suggestion would be:
return Map<MarkerId, Marker>.fromEntries(
markers.map((Marker marker) => MapEntry(marker.id, marker))
);|
|
||
| Map<MarkerId, Marker> _markers; | ||
|
|
||
| dynamic update(Set<Marker> newMarkers) { |
There was a problem hiding this comment.
Will change it to Map<String, dynamic>. It returns the change to be sent to the platform channel
| 'initialCameraPosition': widget.initialCameraPosition?._toMap(), | ||
| 'options': _GoogleMapOptions.fromWidget(widget).toMap(), | ||
| 'options': _googleMapOptions.toMap(), | ||
| 'markerUpdates': _markerUpdateHandler.update(widget.markers), |
There was a problem hiding this comment.
nit: this is not really an update, might make sense to call the creation parameter just 'markers'.
There was a problem hiding this comment.
I am considering renaming MarkerUpdate class to MarkerChange and call this markerChanges. I will still call it markers in creationParams, but elsewhere I will call this markerChanges let me know what you think.
| } | ||
| } | ||
|
|
||
| class _MarkerUpdateHandler { |
There was a problem hiding this comment.
I find the role of this class a little hard to grasp, it does the following:
- Maintains the set of current markers
- Proxy calls to
MarkerUpdatesfor computing and returning the diff for a new markers set. - Provides closures for invoking the marker events on a given marker.
I was also a little confused by the name - the initial mental model I had just from seeing the class name was (this is a marker update handler, it probably handles computing the updates and sending them over the platform channel).
The closure generation is particularly a trick I'd rather avoid if it's not necessary as it is harder to follow.
It might be easier to follow if we keep the markers map as a member of _GoogleMapState, then we could avoid the closures as we could have _onMarkerTap(String markerId) as a method of _GoogleMapState.
It looks like the update computation logic can be done in a pretty short method which we can fit in _updateMarkers.
|
|
||
| /// [Marker] update event with the changes. | ||
| @immutable | ||
| class MarkerUpdate { |
There was a problem hiding this comment.
There's some ambiguity with this being called MarkerUpdate while it can have one [add/remove/update] event type.
There was a problem hiding this comment.
I will rename this MarkerChange
|
|
||
| /// [Marker] update event with the changes. | ||
| @immutable | ||
| class MarkerUpdate { |
There was a problem hiding this comment.
Also - if we keep this class we probably don't want to make it part of the public API.
| ); | ||
| } | ||
|
|
||
| /// TODO(iskakaushik): Diff is sufficient, don't need to send the whole update. |
There was a problem hiding this comment.
TODOs should include a link to a GitHub issue otherwise they are not in our tracking system,.
There was a problem hiding this comment.
Will make an issue for this.
| /// | ||
| /// Used in [GoogleMapController] when the map is updated. | ||
| @immutable | ||
| class MarkerUpdates { |
There was a problem hiding this comment.
Is this essentially a set of MarkerUpdate objects? why do we need a dedicated type for it?
There was a problem hiding this comment.
This does a few more things than just being a wrapper:
- This class provides a nice place to have
.from(previous, current)to compute the diff. - Provides methods for serialization of the update chunk that we need to send over to the platform side.
- Though this is a wrapper around a
Set<MarkerUpdate>, it succinctly captures a snapshot of all updates done to the markers.
| /// | ||
| /// Used in [GoogleMapController] when the map is updated. | ||
| @immutable | ||
| class MarkerUpdates { |
There was a problem hiding this comment.
If we end up keeping this class we probably don't want it to be public API.
There was a problem hiding this comment.
Will make this private.
| remove, | ||
| } | ||
|
|
||
| Map<MarkerId, Marker> _toMap(Set<Marker> markers) { |
There was a problem hiding this comment.
See suggestion on _createMarkerIdMarkerMap, also this seems to be redundant with the logic there?
| } | ||
|
|
||
| void onMarkerTap(String markerIdParam) { | ||
| if (markerIdParam == null) { |
There was a problem hiding this comment.
do we expect this to ever happen? I guess this should be an assertion
| return; | ||
| } | ||
| final MarkerId markerId = MarkerId(markerIdParam); | ||
| _markers[markerId]?.onTap(); |
There was a problem hiding this comment.
Can you add a comment explaining the scenario in which _markers[markerId] can be null
There was a problem hiding this comment.
this cannot happen, got rid of the ?
| Future<void> onPlatformViewCreated(int id) async { | ||
| final GoogleMapController controller = | ||
| await GoogleMapController.init(id, widget.initialCameraPosition); | ||
| final GoogleMapController controller = await GoogleMapController.init( |
There was a problem hiding this comment.
unrelated to this PR: we should make GoogleMapController.init private before we hit API stability.
| ); | ||
| final Marker marker = Marker(markerId, effectiveOptions); | ||
| _markers[markerId] = marker; | ||
| notifyListeners(); |
There was a problem hiding this comment.
Not related to this PR - We should re-evaluate the single-listener model before we hit API stability.
| /// | ||
| /// Used in [GoogleMapController] when the map is updated. | ||
| @immutable | ||
| class MarkerUpdates { |
| } | ||
| } | ||
|
|
||
| /// An icon placed at a particular geographical location on the map's surface. |
There was a problem hiding this comment.
blank line after the summary.
| } | ||
| } | ||
|
|
||
| /// An icon placed at a particular geographical location on the map's surface. |
There was a problem hiding this comment.
Is a marker "an icon"?
Maybe just say in the summary "Marks a geographical location on the map"?
[I see that you just moved this, but might be a good opportunity anyway]
There was a problem hiding this comment.
I meant to suggest that as the summary line.
| /// * 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({ |
There was a problem hiding this comment.
this was previously annotated @VisibleForTesting
There was a problem hiding this comment.
This is now the preferred way to construct markers. Now that we do not have the createWithDefaultSettings factory method
| this.onTap, | ||
| }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); | ||
|
|
||
| /// Used to uniquely identify the [Marker]. |
There was a problem hiding this comment.
nit: Uniquely identifies the marker.
|
|
||
| /// Text content for the info window. | ||
| final InfoWindowText infoWindowText; | ||
| /// Window displayed onTap for a marker. |
There was a problem hiding this comment.
/// A Google Maps InfoWindow.
///
/// The window is displayed when the marker is tapped.
|
|
||
| final Set<MarkerId> markerIdsToRemove = | ||
| prevMarkerIds.difference(currentMarkerIds); | ||
| final Set<Marker> markersToAdd = currentMarkerIds |
There was a problem hiding this comment.
not sure if you've missed the comment or decided to not do the nit (which is ok)
| assert(markersToChange != null); | ||
|
|
||
| /// Computes [MarkerUpdates] given previous and current [Marker]s. | ||
| factory MarkerUpdates.from(Set<Marker> previous, Set<Marker> current) { |
| (Marker marker) => MapEntry<MarkerId, Marker>(marker.markerId, marker))); | ||
| } | ||
|
|
||
| List<dynamic> _serializeMarkerSet(Set<Marker> markers) { |
| } | ||
| } | ||
|
|
||
| /// An icon placed at a particular geographical location on the map's surface. |
There was a problem hiding this comment.
I meant to suggest that as the summary line.
|
|
||
| Set<Marker> _deserializeMarkers(dynamic markers) { | ||
| if (markers == null) { | ||
| // TODO(iskakaushik): Remove this when collection literals makes it to stable. |
There was a problem hiding this comment.
include a link to a GitHub issue
| fakePlatformViewsController.lastCreatedView; | ||
|
|
||
| expect(platformGoogleMap.markersToChange.length, 2); | ||
| expect(platformGoogleMap.markersToChange, equals(cur)); |
There was a problem hiding this comment.
I don't think you need equals here, also no need to compare the length if we're comparing the sets
| 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); |
There was a problem hiding this comment.
my personal preference here would be [m1, m2].toSet() instead of the _toSet method (and soon enough a set literal).
But if you prefer this keep it.
There was a problem hiding this comment.
Filing a github issue to track this: flutter/flutter#28311
| _markerIdCounter++; | ||
| final MarkerId markerId = MarkerId(markerIdVal); | ||
|
|
||
| void _onMarkerTapped() { |
There was a problem hiding this comment.
I think it'll simplify things if we avoid this closure.
We can have a member _onMarkerTapped(MarkerId markerId); and below do:
final Marker marker = Marker(
// ...
onTap: () { _onMarkerTapped(markerId); },
);This way the logic one sees in the _add method is purely about adding a marker.
| _updateSelectedMarker( | ||
| MarkerOptions( | ||
| position: LatLng( | ||
| _updateSelectedMarker((Marker m) { |
There was a problem hiding this comment.
I feel like _updateSelectedMarker is reversing the "reactive" style that we are introducing in this PR. Since this is sample code I'd err on the side of adding a little more duplication but being more clear on the nature of the API(here and in the other "mutation" methods below).
Something like(pseudo):
Marker selectedMarker = markers[_selectedMarker];
setState(() {
markers[_selectedMarker] = selectedMaker.copyWith(position: /* .. */);
});| // ignore: prefer_collection_literals | ||
| Set<Marker>.of( | ||
| <Marker>[ | ||
| Marker( |
…er-api-mod-dart Conflicts: packages/google_maps_flutter/example/lib/scrolling_map.dart
amirh
left a comment
There was a problem hiding this comment.
LGTM modulo 2 nits.
Make sure to only land this after the iOS and Android implementations land.
| /// | ||
| /// Used in [GoogleMapController] when the map is updated. | ||
| class _MarkerUpdates { | ||
| _MarkerUpdates._({ |
|
|
||
| void _changeAnchor() { | ||
| final Offset currentAnchor = _selectedMarker.options.anchor; | ||
| final Marker m = markers[selectedMarker]; |
amirh
left a comment
There was a problem hiding this comment.
LGTM
Don't forget to update the change log and bump the version before submitting. Also make sure to merge this PR after the platform implementations.
…flutter#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 75956c2. * 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
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.