Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

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

Merged
merged 23 commits into from
Feb 27, 2019
Merged

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

merged 23 commits into from
Feb 27, 2019

Conversation

iskakaushik
Copy link
Contributor

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.

Kaushik Iska added 2 commits February 19, 2019 14:24
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: {
Copy link
Contributor

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) {
Copy link
Contributor

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;
Copy link
Contributor

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());
Copy link
Contributor

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 {
Copy link
Contributor

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.
Copy link
Contributor

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 {
Copy link
Contributor

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) {
Copy link
Contributor

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.
Copy link
Contributor

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid dynamic when possible

Copy link
Contributor Author

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),
Copy link
Contributor

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'.

Copy link
Contributor Author

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 {
Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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.
Copy link
Contributor

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,.

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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:

  1. This class provides a nice place to have .from(previous, current) to compute the diff.
  2. Provides methods for serialization of the update chunk that we need to send over to the platform side.
  3. 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 {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will make this private.

Copy link
Contributor

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) {
Copy link
Contributor

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?

@iskakaushik iskakaushik requested a review from amirh February 21, 2019 21:31
_controller.complete(controller);
if (widget.onMapCreated != null) {
widget.onMapCreated(controller);
}
}

void onMarkerTap(String markerIdParam) {
if (markerIdParam == null) {
Copy link
Contributor

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();
Copy link
Contributor

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

Copy link
Contributor Author

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(
Copy link
Contributor

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();
Copy link
Contributor

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 {
Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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]

Copy link
Contributor

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({
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor

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].
Copy link
Contributor

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.
Copy link
Contributor

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.

@bparrishMines bparrishMines changed the title Marker APIs are now widget based (Dart Changes) [google_maps_flutter] Marker APIs are now widget based (Dart Changes) Feb 22, 2019

final Set<MarkerId> markerIdsToRemove =
prevMarkerIds.difference(currentMarkerIds);
final Set<Marker> markersToAdd = currentMarkerIds
Copy link
Contributor

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) {
Copy link
Contributor

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) {
Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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));
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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() {
Copy link
Contributor

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) {
Copy link
Contributor

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

Copy link
Contributor

@amirh amirh left a 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._({
Copy link
Contributor

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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/m/marker/

Copy link
Contributor

@amirh amirh left a 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.

@iskakaushik iskakaushik merged commit d434fee into flutter:master Feb 27, 2019
@iskakaushik iskakaushik deleted the feature/marker-api-mod-dart branch February 27, 2019 15:47
romaluca pushed a commit to romaluca/plugins that referenced this pull request Mar 6, 2019
…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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants