Skip to content

Conversation

@aednlaxer
Copy link
Contributor

This PR adds Advanced markers support to the web implementation of google_maps_flutter.
Approved combined PR: #7882
Approved and merged platform interface PR: #9737
Issue: #155526

Pre-Review Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] page, which explains my responsibilities.
  • I read and followed the [relevant style guides] and ran [the auto-formatter].
  • I signed the [CLA].
  • The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences]
  • I [linked to at least one issue that this PR fixes] in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under[^1].
  • I updated CHANGELOG.md to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under[^1].
  • I updated/added any relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under[^1].
  • All existing and new tests are passing.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR adds support for Advanced Markers to the web implementation of google_maps_flutter. The changes are extensive and well-structured, refactoring the marker handling logic to support both legacy and advanced markers through generics and abstract classes. New tests have been added for the new functionality, and existing ones have been updated. The implementation looks solid, but I've found a couple of critical issues that would prevent the code from compiling or running correctly.

Comment on lines 84 to 85
clusterManagersController: clusterManagersController!
as ClusterManagersController<gmaps.AdvancedMarkerElement>,

Choose a reason for hiding this comment

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

critical

There's a typo here. clusterManagersController is used, but it should be _clusterManagersController. This will cause a compilation error as clusterManagersController is not defined in this scope.

          clusterManagersController: _clusterManagersController!
              as ClusterManagersController<gmaps.AdvancedMarkerElement>,

Copy link
Collaborator

Choose a reason for hiding this comment

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

Obviously this does compile, but it does make it order-sensitive to when the private variable is set, which we don't want, so this should use _clusterManagersController.

Choose a reason for hiding this comment

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

Done.

Comment on lines 84 to 85
clusterManagersController: clusterManagersController!
as ClusterManagersController<gmaps.AdvancedMarkerElement>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Obviously this does compile, but it does make it order-sensitive to when the private variable is set, which we don't want, so this should use _clusterManagersController.

sanitize_html: ^2.0.0
stream_transform: ^2.0.0
web: ">=0.5.1 <2.0.0"
web: ">=1.0.0 <2.0.0"
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can just be ^1.0.0

Choose a reason for hiding this comment

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

Done.

@stuartmorgan-g
Copy link
Collaborator

From triage: @aednlaxer Are you still planning on updating this PR?

@stuartmorgan-g
Copy link
Collaborator

FYI: After merging in main, you'll need to update the license header in all the new files with a new copy from an existing file, as the format has changed slightly.

@aednlaxer
Copy link
Contributor Author

From triage: @aednlaxer Are you still planning on updating this PR?

Yes, please don't close it

@illuminati1911 illuminati1911 force-pushed the feature/advanced_markers_google_maps_flutter_web branch from 9da73a7 to 65b8c81 Compare September 25, 2025 08:48
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

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

Overall, it looks good to me! I just have a few minor code improvements.

options.glyphColor = _getCssColor(circleGlyph.color);
case final TextGlyph textGlyph:
final web.Element element = web.document.createElement('p');
element.innerHTML = textGlyph.text.toJS;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's safer to use element.text:

Suggested change
element.innerHTML = textGlyph.text.toJS;
element.text = textGlyph.text;

Comment on lines 476 to 482
element.setAttribute(
'style',
'color: ${_getCssColor(textGlyph.textColor!)}',
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
element.setAttribute(
'style',
'color: ${_getCssColor(textGlyph.textColor!)}',
);
element.style.color = _getCssColor(textGlyph.textColor!);

Choose a reason for hiding this comment

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

Comment on lines 341 to 350
icon.setAttribute(
'style',
<String>[
if (size != null) ...<String>[
'width: ${size.width.toStringAsFixed(1)}px;',
'height: ${size.height.toStringAsFixed(1)}px;',
],
if (opacity != null) 'opacity: $opacity;',
if (isVisible != null) 'visibility: ${isVisible ? 'visible' : 'hidden'};',
].join(' '),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
icon.setAttribute(
'style',
<String>[
if (size != null) ...<String>[
'width: ${size.width.toStringAsFixed(1)}px;',
'height: ${size.height.toStringAsFixed(1)}px;',
],
if (opacity != null) 'opacity: $opacity;',
if (isVisible != null) 'visibility: ${isVisible ? 'visible' : 'hidden'};',
].join(' '),
final iconStyle = icon.style;
if (size != null) {
iconStyle
..width = '${size.width.toStringAsFixed(1)}px'
..height = '${size.height.toStringAsFixed(1)}px';
}
if (opacity != null) {
iconStyle.opacity = opacity;
}
if (isVisible != null) {
iconStyle.visibility = isVisible ? 'visible' : 'hidden';
}

Choose a reason for hiding this comment

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

Icon type of the web package doesn't have style property either.

Copy link
Contributor

Choose a reason for hiding this comment

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

Making icon of type HTMLImageElement should make it work.

});

@override
void initializeMarkerListener({
Copy link
Contributor

Choose a reason for hiding this comment

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

We typically use the "add listener" terminology in the flutter code base: https://api.flutter.dev/flutter/search.html?q=addlistener

What do you think about renaming this to addMarkerListeners?

Choose a reason for hiding this comment

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

I think it's better to keep the naming consistent. I have changed the function name to addMarkerListener.

if (_infoWindow != null && newInfoWindowContent != null) {
_infoWindow.content = newInfoWindowContent;
if (onTap != null) {
marker.onClick.listen((gmaps.MapMouseEvent event) {
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 need to unsubscribe from these listeners when the marker is removed?

If yes, it can easily be done:

_subscriptions = [
  if (onTap != null) marker.onClick.listen((_) => onTap.call()),

  if (onDragStart != null) marker.onDragstart.listen((event) {
    marker.position = event.latLng;
    onDragStart.call(event.latLng ?? _nullGmapsLatLng);
  }),

  ...
];

and later in void remove():

_subscriptions?.forEach((sub) => sub.cancel);
_subscriptions = null;

Choose a reason for hiding this comment

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

Agreed. 👍

Choose a reason for hiding this comment

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

Done.

required VoidCallback? onTap,
}) {
if (onTap != null) {
marker.onClick.listen((gmaps.MapMouseEvent event) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question about unsubscribing from listeners.

Choose a reason for hiding this comment

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

Done.

@jokerttu jokerttu requested a review from mdebbar October 27, 2025 06:42
@illuminati1911 illuminati1911 force-pushed the feature/advanced_markers_google_maps_flutter_web branch from c1047c3 to f72d139 Compare October 27, 2025 07:09
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

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

Added some suggestions to make element.style and icon.style available.

Comment on lines 526 to 527
final web.Element icon = web.document.createElement('img')
..setAttribute('src', url);
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use a more specific type so we can use icon.style:

Suggested change
final web.Element icon = web.document.createElement('img')
..setAttribute('src', url);
final web.HTMLImageElement icon = web.HTMLImageElement()..src = url;

Choose a reason for hiding this comment

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

Thanks for the suggestion. All changes done.

Comment on lines 550 to 553
final web.Element icon = web.document.createElement('img')..setAttribute(
'src',
ui_web.assetManager.getAssetUrl(iconConfig[1]! as String),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

Comment on lines 578 to 579
final web.Element icon = web.document.createElement('img')
..setAttribute('src', web.URL.createObjectURL(blob as JSObject));
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

Comment on lines 341 to 350
icon.setAttribute(
'style',
<String>[
if (size != null) ...<String>[
'width: ${size.width.toStringAsFixed(1)}px;',
'height: ${size.height.toStringAsFixed(1)}px;',
],
if (opacity != null) 'opacity: $opacity;',
if (isVisible != null) 'visibility: ${isVisible ? 'visible' : 'hidden'};',
].join(' '),
Copy link
Contributor

Choose a reason for hiding this comment

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

Making icon of type HTMLImageElement should make it work.

Comment on lines 476 to 482
final web.Element element = web.document.createElement('p');
element.text = textGlyph.text;
if (textGlyph.textColor != null) {
element.setAttribute(
'style',
'color: ${_getCssColor(textGlyph.textColor!)}',
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a HTMLParagraphElement type will allow you to use element.style:

Suggested change
final web.Element element = web.document.createElement('p');
element.text = textGlyph.text;
if (textGlyph.textColor != null) {
element.setAttribute(
'style',
'color: ${_getCssColor(textGlyph.textColor!)}',
);
final web.HTMLParagraphElement element = web.HTMLParagraphElement();
element.text = textGlyph.text;
if (textGlyph.textColor != null) {
element.style.color = _getCssColor(textGlyph.textColor!);

Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

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

LGTM! Thank you!

@illuminati1911 illuminati1911 force-pushed the feature/advanced_markers_google_maps_flutter_web branch from 4cbf9d7 to 71404f1 Compare November 10, 2025 02:27
@illuminati1911 illuminati1911 force-pushed the feature/advanced_markers_google_maps_flutter_web branch from 71404f1 to f30050c Compare November 10, 2025 03:29
final gmaps.Size gmapsSize = gmaps.Size(size.width, size.height);
icon.size = gmapsSize;
icon.scaledSize = gmapsSize;
// Sets the size and style of the [icon] element.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doc-style (i.e., caller-facing, rather than implementation-detail-describing) comments should still use /// even if the methods aren't public.

BitmapDescriptor bitmapDescriptor, {
required double? opacity,
required bool isVisible,
required double? rotation,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are two of these parameters both required and nullable? What does explicitly passing null mean conceptually? This should be explained in the doc comment.

..visibility = isVisible ? 'visible' : 'hidden'
..opacity = opacity?.toString() ?? '1.0'
..transform = rotation != null ? 'rotate(${rotation}deg)' : '';
return htmlElement;
Copy link
Collaborator

Choose a reason for hiding this comment

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

These implementations are all quite long, which makes it hard to see the overall structure of the method being essentially a switch on bitmapDescriptor. This would be much easier to follow if each of the bodies of the top-level ifs were extracted to their own helpers, and this method were just the branching.

(final BytesMapBitmap bytesMapBitmap) => _bitmapBlobUrlCache.putIfAbsent(
bytesMapBitmap.byteData.hashCode,
() {
final web.Blob blob = web.Blob(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this have the same TODO as the essentially identical code above?


/// Gets marker Id from a [marker] object.
MarkerId getMarkerId(Object? marker) {
final JSObject object = marker! as JSObject;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is the parameter nullable if the first line is going to assert that it's not null?

/// Creates a `MarkerController`, which wraps a [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its associated [gmaps.InfoWindow].
/// The `MarkerController` class wraps a [gmaps.AdvancedMarkerElement]
/// or [gmaps.Marker], how it handles events, and its associated (optional)
/// [gmaps.InfoWindow] widget.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should explain what T and O are in the context of the class.


@override
void showInfoWindow() {
assert(_marker != null, 'Cannot `showInfoWindow` on a `remove`d Marker.');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this a (debug only) assert rather than a StateError?

/// advanced [gmaps.AdvancedMarkerElement] class.
///
/// [T] must extend [JSObject]. It's not specified in code because our mocking
/// framework does not support mocking JSObjects.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here too, the role of T and O should be explained.

## 0.6.0

* Adds Advanced markers support.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please list the breaking changes (following repo style), and provide more details about the new APIs, giving someone reading this enough pointers that they could figure out what APIs to go look at to adopt the new markers.

```

Now you should be able to use the Google Maps plugin normally.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it would be helpful to add a new section discussing legacy vs advanced markers, with links to relevant underlying SDK docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants