Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[google_maps_flutter] Add Advanced Markers support #7882

Open
wants to merge 91 commits into
base: main
Choose a base branch
from

Conversation

aednlaxer
Copy link

@aednlaxer aednlaxer commented Oct 16, 2024

This PR adds Advanced Markers support to google_maps_flutter as discussed in #155526. The document from the issue offers several options to implement Advanced Markers support, this PR uses option 1 (Advanced Marker Dart class is a subclass Marker class).

Summary of changes:

  • Add map capability check
  • Add AdvancedMarker class
  • Add MarkerCollisionBehavior enum to control Advanced Marker's behavior when it collides with another marker
  • Add PinConfig bitmap descriptor for customizing Advanced Marker's pin and icon
  • Add markerType parameter to indicate that Advanced Markers should be used
  • Rename cloudMapId to mapId

Notes:

  • native implementations need to know what kind of marker should be used, this is needed to use correct marker options, cluster manager and marker controller. Indicating marker type is done by using a markerType option when creating a GoogleMap (could be marker or advancedMarker). Default option is marker
  • cloudMapId is deprecated in favor of mapId. New name follows SDKs, documentation and Cloud Console naming.
  • web implementation uses generics because gmaps.Marker and gmaps.AdvancedMarkerElement are not related to each other and should be handled differently
  • legacy markers are deprecated but still supported in Maps Javascript API. google_maps_flutter still uses them by default in this PR because of backward-compatibility. #130472 is related, package users will be able to use Advanced Markers to fix the deprecation warning
  • Advanced Markers example on iOS doesn't show advanced markers, seems to be a known issue
  • Using Flutter widget as Advanced Marker icon (so called View Marker) is not part of this PR, this was discussed in the document and later removed as we agreed that it should be a separate issue

Resolves #155526

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Copy link
Contributor

@reidbaker reidbaker left a comment

Choose a reason for hiding this comment

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

High level android code review. I am concerned by having to runtime type check class.

That said thank you for opening this pull request.

*/
private void initializeRenderer(ClusterManager<MarkerBuilder> clusterManager) {
final ClusterRenderer<MarkerBuilder> renderer = clusterManager.getRenderer();
if (renderer.getClass() == MarkerClusterRenderer.class
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this reflection here? Can we use non null instead as a signal here?

Copy link
Author

Choose a reason for hiding this comment

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

I've removed this check - previously initializeRenderer (it was named differently) was called multiple times, now it's called once and there's no need to check if a renderer has been previously assigned

@@ -225,6 +256,35 @@ protected void onClusterItemRendered(@NonNull T item, @NonNull Marker marker) {
}
}

/** AdvancedMarkerClusterRenderer is a ClusterRenderer that supports AdvancedMarkers */
@VisibleForTesting
static class AdvancedMarkerClusterRenderer<T extends MarkerBuilder>
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be a different class? If it does can it extend MarkerClusterRenderer?

I worry about keeping the two in sync.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it does. android-maps-utils have two different renderers - one for each marker type (legacy and advanced). This new class extends DefaultAdvancedMarkersClusterRenderer which is meant to be used with AdvancedMarkers. As far as I remember, android-maps-utils recreates cluster markers under the hood so it has to know which marker type to use

@@ -980,6 +984,18 @@ public Boolean isInfoWindowShown(@NonNull String markerId) {
return lastSetStyleSucceeded;
}

@Override
public @NonNull Boolean isAdvancedMarkersAvailable() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be bool instead of Boolean if it is non null?

Copy link
Author

Choose a reason for hiding this comment

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

Looks like it can't be done like this. This code uses Pigeon-generated code and Dart's bool isAdvancedMarkersAvailable() is translated into Java's @NonNull Boolean isAdvancedMarkersAvailable();

final String cloudMapId = mapConfig.getCloudMapId();
if (cloudMapId != null) {
builder.setMapId(cloudMapId);
final String mapId = mapConfig.getMapId();
Copy link
Contributor

Choose a reason for hiding this comment

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

For my own education can you share why we would want to use mapId instead of cloudMapId and why the change is safe (wont cause a regression in existing apps)

Copy link
Author

Choose a reason for hiding this comment

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

I've added a comment about renaming it here.. It's safe because cloudMapId can still be used by existing clients, it's marked as deprecated and uses mapId under the hood

@@ -143,6 +143,9 @@ public void setZIndex(float zIndex) {
marker.setZIndex(zIndex);
}

@Override
public void setCollisionBehavior(int collisionBehavior) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a javadoc that describes the valid collisionBehavior int's or at least where they are defined.
Better yet convert int to something typesafe

@stuartmorgan stuartmorgan added the triage-ios Should be looked at in iOS triage label Nov 21, 2024
Copy link
Contributor

@stuartmorgan stuartmorgan left a comment

Choose a reason for hiding this comment

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

I've only looked at the app-facing and platform interface packages so far, but there are some fundamental structure questions at the interface layer.


/// Color of the default glyph (circle)
final Color? color;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are all of the different types of Glyph a single class with fields that are almost all unique to a single type, rather than using subclasses like, e.g., BitmapDescriptor?

Copy link
Author

Choose a reason for hiding this comment

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

This is a really good suggestion, thanks. I've changed all three named constructors to be separate classes extending BitmapDescriptor. They're all now in bitmap.dart

///
/// See https://developers.google.com/maps/documentation/get-map-id
/// for more details.
@Deprecated('cloudMapId is deprecated. Use mapId instead')
final String? cloudMapId;
Copy link
Contributor

Choose a reason for hiding this comment

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

The intent is to renamed cloudMapId, right? If so, the field should be removed, and it should store to and read from mapId, rather than having two potentially conflicting fields.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, the intent is to rename cloudMapId to mapId, I've removed the field. Do you think it's okay to rename it and it should be done in this PR?

Why it should be renamed:

  • mapId is what Google Maps platform implementations use
  • "Map ID" is what Cloud Console uses on Map styles page
  • cloudMapId feels a bit like it's used for cloud styling which is true but after introduction of Advanced markers map ID is required to make Advanced markers work even if map is not using a custom style.

Copy link
Contributor

Choose a reason for hiding this comment

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

Removing cloudMapId from the interface (as in the current version of the PR) is a breaking change, which isn't something we want. What I was suggesting was replacing the field with a getter that reads from mapId, as that preserves the public API without having two difference sources of truth.

Copy link
Author

Choose a reason for hiding this comment

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

This should be fixed now

@stuartmorgan
Copy link
Contributor

Looks like some platform reviewers were dropped at some point on accident.

@stuartmorgan stuartmorgan added triage-android Should be looked at in Android triage triage-web Should be looked at in web triage labels Jan 7, 2025
@@ -121,6 +121,21 @@ the `GoogleMap`'s `onMapCreated` callback.
The `GoogleMap` widget should be used within a widget with a bounded size. Using it
in an unbounded widget will cause the application to throw a Flutter exception.

### Advanced Markers

[Advanced Markers](https://developers.google.com/maps/documentation/javascript/advanced-markers/overview) are map markers that offer extra customization options. [Map Id](https://developers.google.com/maps/documentation/get-map-id) is required in order to use Advanced 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: The official docs refer to this as "Map ID" not "Map Id".

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, fixed

packages/google_maps_flutter/google_maps_flutter/README.md Outdated Show resolved Hide resolved

/// Whether map supports advanced markers. Null indicates capability check
/// is in progress
bool? _isAdvancedMarkersAvailable;
Copy link
Contributor

Choose a reason for hiding this comment

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

That's unfortunate 😐. I agree though, matching the underlying SDK makes sense here.

}

@override
int get hashCode => markerId.hashCode;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hrm, maybe that's intentional then. I'd need to dig into the SDK a lot more to be sure, but being consistent for now sounds good.

///
/// See https://developers.google.com/maps/documentation/get-map-id
/// for more details.
@Deprecated('cloudMapId is deprecated. Use mapId instead')
final String? cloudMapId;
Copy link
Contributor

Choose a reason for hiding this comment

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

Removing cloudMapId from the interface (as in the current version of the PR) is a breaking change, which isn't something we want. What I was suggesting was replacing the field with a getter that reads from mapId, as that preserves the public API without having two difference sources of truth.

@@ -28,14 +31,19 @@ import 'scrolling_map.dart';
import 'snapshot.dart';
import 'tile_overlay.dart';

/// Map ID is required for some examples to use advanced markers.
const String? _mapId = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm having trouble reconciling the comment and the line of code; if it's required for the examples, why is it null?

Is the idea that someone should put in their own map ID here and run the modified example? If so, the comment should be explicit about that.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, the idea is that someone should put their own ID. I've updated the comment to

/// Place your map ID here. Map ID is required for pages that use advanced
/// markers.

Hope it makes it clear what needs to be done and why.

final String? cloudMapId;

/// Indicates whether map should use [AdvancedMarker]s or [Marker]s.
final MarkerType markerType;
Copy link
Contributor

Choose a reason for hiding this comment

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

This still needs to be updated.

@rajubhusani
Copy link

rajubhusani commented Jan 11, 2025

I see Advanced merker icons are not rendering properly on the map.
Unhandled Exception: PlatformException(java.lang.IllegalArgumentException: Unable to interpret pin config as a valid image., IllegalArgumentException, Cause: java.lang.NullPointerException: null reference, Stacktrace: java.lang.IllegalArgumentException: Unable to interpret pin config as a valid image.

@aednlaxer
Copy link
Author

aednlaxer commented Jan 13, 2025

@rajubhusani

I see Advanced merker icons are not rendering properly on the map. Unhandled Exception: PlatformException(java.lang.IllegalArgumentException: Unable to interpret pin config as a valid image., IllegalArgumentException, Cause: java.lang.NullPointerException: null reference, Stacktrace: java.lang.IllegalArgumentException: Unable to interpret pin config as a valid image.

Hi, thanks for checking it out. Could please provide more details?

  • Did you use one of the new examples from this PR? Which one?
  • Did you add your API key and map ID before running the app?
  • Did you build your own sample to test advanced markers? Can you share it or provide code that could help reproducing the issue?
  • What kind of image do you use? Would it be possible to test if this image works with legacy (non-advanced) markers?
  • Did you stop the Android app before running the code? Running app should be stopped and started again after checking out this PR's code. The reason is the platform-specific code that won't work using Flutter's hot-restart and should only work if native (Android) app is restarted.

Copy link
Member

@ditman ditman left a comment

Choose a reason for hiding this comment

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

Hey, thanks for the code! I took a quick look at the core plugin, the platform interface, but specially the web implementation.

Most of the stuff is stylistic choices, but I have a question:

  • Why do we even need the MarkerType when creating a Map? It seems that it can be inferred the first time a Marker is used, and the availability of Advanced Markers?

And an objection to the doXXXOnMarkerType pattern, which IMO should be proper inheritance (like you did on AdvancedMarker extends Marker).

Thanks for the PR, this is a highly requested feature!

Comment on lines 38 to 39
@override
Marker createMarker(
Copy link
Member

Choose a reason for hiding this comment

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

This is smart, and reduces code repetition, but I'm not sure people looking at an example will want to unravel this inheritance. This might be more useful for end users if there's a little bit of duplication across the examples?

Copy link
Author

Choose a reason for hiding this comment

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

I've updated all advanced examples not to do that, should be simpler now. Advanced marker icons example now shows several pin config variations

Comment on lines 127 to 128
final Marker resetOld =
widget.copyWithSelectedStated(markers[previousMarkerId]!, false);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using !, I'd use an assert(marker != null, 'Some descriptive message'). Null assertion errors are quite cryptic (and more in the web, and even more in wasm :P), so I tend to avoid them if practical.

@rajubhusani
Copy link

@rajubhusani

I see Advanced merker icons are not rendering properly on the map. Unhandled Exception: PlatformException(java.lang.IllegalArgumentException: Unable to interpret pin config as a valid image., IllegalArgumentException, Cause: java.lang.NullPointerException: null reference, Stacktrace: java.lang.IllegalArgumentException: Unable to interpret pin config as a valid image.

Hi, thanks for checking it out. Could please provide more details?

  • Did you use one of the new examples from this PR? Which one?
  • Did you add your API key and map ID before running the app?
  • Did you build your own sample to test advanced markers? Can you share it or provide code that could help reproducing the issue?
  • What kind of image do you use? Would it be possible to test if this image works with legacy (non-advanced) markers?
  • Did you stop the Android app before running the code? Running app should be stopped and started again after checking out this PR's code. The reason is the platform-specific code that won't work using Flutter's hot-restart and should only work if native (Android) app is restarted.

@aednlaxer, Please find the responses,
I just cloned the code from your branch "CodemateLtd:feature/google-maps-advanced-markers-support". And directly ran the Android app from the google_maps_flutter_android/example project. I have not made any changes to that. I have used my own API key setup. All the options are working on the example except the Custom Marker & Cluster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p: google_maps_flutter platform-android platform-ios platform-web triage-android Should be looked at in Android triage triage-ios Should be looked at in iOS triage triage-web Should be looked at in web triage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Advanced Marker support to google_maps_flutter
6 participants