-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[google_maps_flutter] Marker APIs are now widget based (Dart Changes) #1239
[google_maps_flutter] Marker APIs are now widget based (Dart Changes) #1239
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.
initialCameraPosition: CameraPosition( | ||
target: center, | ||
zoom: 11.0, | ||
), | ||
gestureRecognizers: | ||
<Factory<OneSequenceGestureRecognizer>>[ | ||
markers: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussed offline but leaving as a reminder - we can't use set literals here yet.
_googleMapOptions = newOptions; | ||
} | ||
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
if (updates.isEmpty) { | ||
return; | ||
} | ||
final GoogleMapController controller = await _controller.future; | ||
controller._updateMapOptions(updates); | ||
final MarkerControllers markerControllers = await _markerControllers.future; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: markersController
(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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoid dynamic when possible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will change it to Map<String, dynamic>
. It returns the change to be sent to the platform channel
_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(), | ||
'markerUpdates': _markerUpdateHandler.update(widget.markers), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this is not really an update, might make sense to call the creation parameter just 'markers'
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
_controller.complete(controller); | ||
if (widget.onMapCreated != null) { | ||
widget.onMapCreated(controller); | ||
} | ||
} | ||
} | ||
|
||
class _MarkerUpdateHandler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
MarkerUpdates
for 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will rename this MarkerChange
|
||
/// [Marker] update event with the changes. | ||
@immutable | ||
class MarkerUpdate { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODOs should include a link to a GitHub issue otherwise they are not in our tracking system,.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will make an issue for this.
/// | ||
/// Used in [GoogleMapController] when the map is updated. | ||
@immutable | ||
class MarkerUpdates { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this essentially a set of MarkerUpdate objects? why do we need a dedicated type for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we end up keeping this class we probably don't want it to be public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will make this private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still need to do this
remove, | ||
} | ||
|
||
Map<MarkerId, Marker> _toMap(Set<Marker> markers) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See suggestion on _createMarkerIdMarkerMap, also this seems to be redundant with the logic there?
_controller.complete(controller); | ||
if (widget.onMapCreated != null) { | ||
widget.onMapCreated(controller); | ||
} | ||
} | ||
|
||
void onMarkerTap(String markerIdParam) { | ||
if (markerIdParam == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment explaining the scenario in which _markers[markerId] can be null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still need to do this
} | ||
} | ||
|
||
/// An icon placed at a particular geographical location on the map's surface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was previously annotated @VisibleForTesting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now the preferred way to construct markers. Now that we do not have the createWithDefaultSettings
factory method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohh right
}) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); | ||
|
||
/// Used to uniquely identify the [Marker]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ping
(Marker marker) => MapEntry<MarkerId, Marker>(marker.markerId, marker))); | ||
} | ||
|
||
List<dynamic> _serializeMarkerSet(Set<Marker> markers) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List<Map<String, dynamic>>
} | ||
} | ||
|
||
/// An icon placed at a particular geographical location on the map's surface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
…er-api-mod-dart Conflicts: packages/google_maps_flutter/example/lib/scrolling_map.dart
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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._({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used?
} | ||
|
||
void _changeAnchor() { | ||
final Offset currentAnchor = _selectedMarker.options.anchor; | ||
final Marker m = markers[selectedMarker]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/m/marker/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.