Skip to content

[google_maps_flutter] Support Google Maps onMapLoaded - #10714

Closed
hearsilent wants to merge 16 commits into
flutter:mainfrom
hearsilent:dev/maps
Closed

hearsilent wants to merge 16 commits into
flutter:mainfrom
hearsilent:dev/maps

Conversation

@hearsilent

@hearsilent hearsilent commented Jan 5, 2026 •

Copy link
Copy Markdown

Implements the GoogleMap.OnMapLoadedCallback interface and triggers the onMapLoaded event via platform channels on Android and iOS, with corresponding handling on Web. Updates the generated Pigeon message files and platform interface to support the new event, enabling Flutter apps to listen for when the map has fully loaded across all supported platforms.

Fixes flutter/flutter#99610

Pre-Review Checklist

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

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2 ↩3

Implements the GoogleMap.OnMapLoadedCallback interface and triggers the onMapLoaded event in the platform channels for both Android and iOS. Updates generated Pigeon message files and platform interface to support the new event, enabling Flutter apps to listen for when the map has fully loaded.
Introduces a new test in both Android and iOS Google Maps Flutter plugins to verify that the onMapLoaded event is sent to the correct stream for a given mapId. This ensures proper event delivery from the native side to Dart.
Updates package versions and changelogs to reflect the new feature.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for the onMapLoaded callback in the google_maps_flutter plugin for both Android and iOS. The changes are well-implemented across the platform interface, Android, and iOS packages. The platform interface is correctly extended with the new MapLoadedEvent and onMapLoaded stream. The native implementations use the appropriate platform-specific callbacks (OnMapLoadedCallback for Android and mapViewDidFinishTileRendering: for iOS) and communicate with the Dart side via updated Pigeon message definitions. New tests have been added to verify the functionality on both platforms, and all relevant package versions and changelogs have been updated accordingly. The code is clean, consistent, and follows existing patterns.

@stuartmorgan-g

Copy link
Copy Markdown
Contributor

Thanks for the contribution!

Two initial high-level notes while this waits for review:

Introduces an onMapLoaded callback to the GoogleMap widget, allowing clients to be notified when map tiles have finished loading. Updates the controller and web implementation to support the new event, adds related tests, and demonstrates usage in the example app.
Added dependency_overrides to various pubspec.yaml files across google_maps_flutter packages to enable local development and testing of federated plugins. These changes are for testing and initial review only and should not be merged into the main branch, as indicated by the included comments and reference to the Flutter contributing guidelines.
@hearsilent

Copy link
Copy Markdown
Author

Thanks for the notes!

Both points have been fully addressed:

  • The federated plugin change instructions have been followed, and CI should now run successfully.
  • Web is now handled appropriately, so clients relying on this callback will not be silently broken.

Please let me know if there’s anything else to adjust while this is under review.

@stuartmorgan-g

Copy link
Copy Markdown
Contributor

The CI failures all look like real failures now, which will need to be resolved (other than the expected federated safety check).

Implements the onMapLoaded and onGroundOverlayTap event streams in MethodChannelGoogleMapsFlutter. Updates the changelog to reflect these additions.
Updated the test to exclude WebMapReadyEvent from the capturedEvents stream, ensuring only relevant events are considered during testing.
@hearsilent

Copy link
Copy Markdown
Author

Thanks for the heads up. All CI failures have now been resolved, except for the expected federated safety check.

@vashworth vashworth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

iOS side LGTM

@LongCatIsLooong LongCatIsLooong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The iOS implementation lgtm (modulo a potentially broken link).

}

@override
Stream<MapLoadedEvent> onMapLoaded({required int mapId}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not familiar with the API conventions of this package, but each event in the Stream basically carries no information so it might as well be Void. It looks to me that the MapLoadedEvent type only exists so that this implementation can use it to filter the event stream and extract the relevant events, and MapLoadedEvent is not useful to the caller of this API at all. If that's indeed the case, would it be possible to hide the MapLoadedEvent type? Or is it made public for subclassing?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I assume with the current interface, to provide a "start loading" event stream in the future would require adding a new onMapLoading method to this interface. I would expect a lot of use cases of this API would also involve listening to the "Map started loading" event stream and combining the two streams (e.g., I want to perform asynchronous work on map load, and cancel that work if the map starts loading again).

IMO it might be a bit more ergonomic to return a Stream that gives you both start and finish events (so something like Stream<bool>, or the boolean flag can be added to MapLoadEvent), or even a ValueNotifier<bool> (but now the caller would have to manage the lifespan of the ValueNotifier so I'm not sure about that).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah it looks like this is a low-level interface and most app developers don't have to use this directly. Feel free to ignore the comments then.

Corrected the documentation link for GMSMapViewDelegate.mapViewDidFinishTileRendering in the GoogleMap widget to point to the updated iOS SDK reference.
@stuartmorgan-g stuartmorgan-g added triage-android Should be looked at in Android triage triage-web Should be looked at in web triage labels Jan 20, 2026

@mdebbar mdebbar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM with a small suggestion!

Moves the WebMapReadyEvent trigger to the onTilesloaded listener and ensures it is only fired the first time tiles are loaded. This consolidates event handling and prevents duplicate ready events.
@stuartmorgan-g stuartmorgan-g removed the triage-web Should be looked at in web triage label Feb 2, 2026

@stuartmorgan-g stuartmorgan-g left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly looks good, but one important question about the API behavior.

updateMapStyle(initialMapStyle);
initialMapStyle = null;
}
googleMap.setOnMapLoadedCallback(this);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This only ever results in one callback. Is that the desired behavior? Does it match the implementation on the other platforms?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, this is the expected behavior.
OnMapLoadedCallback is invoked once per registration when the map has finished rendering all required tiles and labels. It is not a continuous callback and won’t fire again unless re-registered.

This matches the documented behavior and is consistent with other platforms.
https://developers.google.com/maps/documentation/navigation/android-sdk/reference/com/google/android/gms/maps/GoogleMap.OnMapLoadedCallback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, this is the expected behavior.

Expected by whom? The Android docs explicitly mention how to get more callbacks (suggesting they think people may want that), and the web API appears to have the opposite behavior. Is there evidence suggesting that this variant is the one people want?

I'm also confused by the platform interface API if this is the expected behavior. Why is a stream being returned for something that can only happen one time?

OnMapLoadedCallback is invoked once per registration when the map has finished rendering all required tiles and labels. It is not a continuous callback and won’t fire again unless re-registered.

This matches the documented behavior and is consistent with other platforms. https://developers.google.com/maps/documentation/navigation/android-sdk/reference/com/google/android/gms/maps/GoogleMap.OnMapLoadedCallback

I know that's what the Android API says; that's why I asked in the first place. The iOS API, on the other hand, doesn't say (and the existence of mapViewDidStartTileRendering: suggests the opposite since I'm not sure why it would be needed for a one-time call during map initialization). What scenarios have you tested on iOS to ensure that it doesn't retrigger? Large moves? Tile changes?

Also, nothing in the public Dart APIs added in this PR say what the intended behavior is.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sorry about that — I was mistaken earlier. Thanks for the detailed follow-up.

I’ve now updated the API documentation to clearly state that this is a one-time callback per registration, not a continuous or repeatable event. The Dart-facing docs no longer imply stream-like behavior.

Appreciate you calling this out and helping catch the inconsistency.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’ve now updated the API documentation to clearly state that this is a one-time callback per registration, not a continuous or repeatable event.

That is not at all what the docs say in the updated version. Instead it says it's just exposing all the platform differences directly to the client, but without any of the associated platform behavior like iOS's mapViewDidStartTileRendering:.

At this point, it seems like there's clearly a non-trivial question of what exactly the desired semantics of a coherent cross-platform API are, and how that could be implemented on each platform, so before continuing to iterate on the PR the next step would be a short design document discussing the options and their tradeoffs, and we can get alignment on that at a high level before proceeding here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Problem Statement

The google_maps_flutter plugin needs an onMapLoaded callback to notify developers when the map has finished rendering its tiles. However, the underlying native Google Maps SDKs have fundamentally different semantics for their "map loaded" events:

Platform Native API Fires Once? Fires After Camera Move?
Android GoogleMap.OnMapLoadedCallback Yes (per registration) No (must re-register)
iOS mapViewDidFinishTileRendering: No Yes
Web tilesloaded event No Yes

This creates a design question: what should the cross-platform Flutter API promise to developers?

Native API Details

Android: GoogleMap.OnMapLoadedCallback

  • Called once when all visible tiles have been loaded and all labeling is complete.
  • This is a one-shot callback: after it fires, it is automatically unregistered.
  • To receive subsequent notifications, developers must call setOnMapLoadedCallback() again.
  • Will not fire if the map never reaches a fully loaded state (e.g., connectivity issues, continuous interaction).
  • Runs on the Android UI thread.

iOS: mapViewDidFinishTileRendering: (GMSMapViewDelegate)

  • Called whenever tile rendering completes, which can happen many times: after initial load, after panning, zooming, or map data updates.
  • This is a continuous delegate method: it fires every time the map finishes rendering a set of tiles.
  • iOS also offers mapViewSnapshotReady:, which fires when the map is fully stable (tiles loaded + labels rendered + camera idle + overlays rendered). This is a stricter "everything is done" signal.

Web (JavaScript API): tilesloaded event

  • Fires every time visible tiles finish loading — initial load, pan, zoom, map type changes.
  • This is a continuous event listener.
  • Will not fire if no new tiles need to be fetched (e.g., all tiles already cached).
  • The idle event is a separate concept that fires when the map becomes completely idle.

Design Options

Option A: One-Time Callback ✅ (Recommended)

Guarantee: onMapLoaded fires exactly once after the initial map tiles finish rendering.

Platform Approach
Android Use OnMapLoadedCallback directly (native one-shot behavior).
iOS Listen to mapViewDidFinishTileRendering:, fire once, ignore subsequent calls.
Web Listen to tilesloaded, fire once (using a flag), ignore subsequent events.

Pros:

  • Simple, predictable contract — developers know the callback fires once.
  • Covers the most common use case: hiding a loading spinner, enabling UI, or taking a screenshot.
  • Consistent with the existing onMapCreated callback pattern.
  • Non-breaking path to Option D if repeatable events are needed later.

Cons:

  • Cannot detect subsequent tile loads (but onCameraIdle covers most of that need).

Option B: Repeatable Callback

Guarantee: onMapLoaded fires every time the map finishes rendering tiles.

Platform Approach
Android Re-register OnMapLoadedCallback after each invocation.
iOS Use mapViewDidFinishTileRendering: directly.
Web Use tilesloaded event directly.

Pros:

  • Exposes full native capability; supports advanced use cases (e.g., showing a loading indicator during pans/zooms).

Cons:

  • Android re-registration is non-standard; potential edge cases (gap between callback firing and re-registration during rapid interactions, undocumented behavior differences).
  • Performance concerns: frequent callbacks could cause issues if developers do expensive work.
  • Even with re-registration, subtle timing/frequency differences across platforms may remain.

Option C: Document Platform Differences

Guarantee: None — each platform exposes its native behavior, documented in the API.

This is the approach taken by the current PR. Android fires once; iOS/Web fire repeatedly.

Pros:

  • Simplest implementation.

Cons:

  • Inconsistent API contract violates Flutter's cross-platform philosophy.
  • Shifts the burden of handling differences to every developer.

Option D: Dual API

Provide both semantics through different API surfaces:

  • onMapLoaded (VoidCallback?): Fires once after initial rendering.
  • onTilesRendered (stream or separate callback): Fires every time tiles finish rendering.

Pros:

  • Best of both worlds; clean separation of concerns.

Cons:

  • Larger API surface.
  • Same Android re-registration concerns for the repeatable stream.
  • Could be added later on top of Option A without breaking changes.

Recommendation: Option A

Option A (One-Time Callback) is recommended because:

  1. Strongest cross-platform consistency: All platforms guarantee exactly one callback.
  2. Addresses the primary use case: Knowing when the map is initially ready.
  3. Simplest and safest: No Android re-registration edge cases, no performance concerns.
  4. Forward-compatible: A repeatable stream (onTilesRendered) can be added later via Option D without modifying onMapLoaded.
  5. Follows existing patterns: Consistent with onMapCreated (also fires once).

Implementation Changes

  • Android: No changes — native OnMapLoadedCallback already fires once.
  • iOS: Add a boolean flag to GoogleMapController.m to fire didFinishRenderingMap only once.
  • Web: Only emit MapLoadedEvent on the first tilesloaded event (already tracks _tilesLoaded flag).
  • API docs: Update onMapLoaded doc comment to describe consistent one-time behavior.

onMapLoaded vs. onCameraIdle

These are complementary signals:

  • onCameraIdle fires when camera animations complete — tiles may still be loading.
  • onMapLoaded fires when visual content is rendered — the map is visually ready.

Developers needing ongoing state change detection should use onCameraIdle, which fires on all platforms after camera movements complete.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@hearsilent Please follow the design doc process I linked to. GitHub's linear commenting system is not suitable for design doc feedback, which is why we have a separate system for design docs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@stuartmorgan-g Got it… I’ll go through the design doc process you shared and try to get everything set up properly. This might take me a bit, but I’ll work through it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@stuartmorgan-g I’ve already submitted the design doc for this proposal:
https://flutter.dev/go/onmaploaded

Please let me know if there’s anything specific you’d like me to update or clarify in the document.

@stuartmorgan-g stuartmorgan-g left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(That was meant to be a "request changes" review.)

Update the onMapLoaded documentation to state it is called when all visible tiles have been loaded and rendered. Remove platform-specific mapping references so the comment focuses on the observable behavior (tiles loaded and rendered) rather than platform APIs.
@hearsilent

Copy link
Copy Markdown
Author

@stuartmorgan-g
Got it, thanks for clarifying.
The requested changes have been addressed.

Expand onMapLoaded documentation to detail platform-specific behavior: Android invokes it once after initial visible tiles load, while iOS and Web may call it multiple times (e.g., after camera moves or zooms). Also recommends using onCameraIdle for cross-platform detection of map stabilization.
@camsim99

Copy link
Copy Markdown
Contributor

@hearsilent Do you mind taking a look at the merge conflicts so it will be easier to review this PR? Thank you!

@stuartmorgan-g

Copy link
Copy Markdown
Contributor

@camsim99 The Android conflicts are just the usual version/changelog conflicts, which we generally don't ask people to resolve during incremental review due to the frequency of conflicts there.

@camsim99

Copy link
Copy Markdown
Contributor

@camsim99 The Android conflicts are just the usual version/changelog conflicts, which we generally don't ask people to resolve during incremental review due to the frequency of conflicts there.

Oh I missed that, sorry!

@stuartmorgan-g

Copy link
Copy Markdown
Contributor

From triage: @hearsilent Are you planning on following up with a design document as requested above?

@hearsilent

Copy link
Copy Markdown
Author

@stuartmorgan-g I have this on my radar! I'm a bit busy with other tasks at the moment, but I’ll follow up with the design doc over the next few days.

@gmackall gmackall removed the triage-android Should be looked at in Android triage label Mar 17, 2026
@gmackall

Copy link
Copy Markdown
Member

From android triage: removing triage label based on discussion that a design document is in the works

parlough pushed a commit to flutter/website that referenced this pull request Mar 22, 2026
_Description of what this PR is changing or adding, and why:_

Adds a `flutter.dev/go` redirect for the public [onMapLoaded
Cross-Platform
Semantics](https://docs.google.com/document/d/1ogwmAQOjbX6UoHnsQ1sQG2P0LRU2KRBED64jnjtBpIQ/edit?usp=sharing).

This is needed so the design doc follows the Flutter design doc process
and can be referenced from the tracking issue and review discussions.

_Issues fixed by this PR (if any):_

flutter/flutter#99610

_PRs or commits this PR depends on (if any):_

flutter/packages#10714

## Presubmit checklist

- [x] If you are unwilling, or unable, to sign the CLA, even for a
_tiny_, one-word PR, please file an issue instead of a PR.
- [x] If this PR is not meant to land until a future stable release,
mark it as draft with an explanation.
- [x] This PR follows the [Google Developer Documentation Style
Guidelines](https://developers.google.com/style)—for example, it doesn't
use _i.e._ or _e.g._, and it avoids _I_ and _we_ (first-person
pronouns).
- [x] This PR uses [semantic line
breaks](https://github.com/dart-lang/site-shared/blob/main/doc/writing-for-dart-and-flutter-websites.md#semantic-line-breaks)
  of 80 characters or fewer.
@stuartmorgan-g

Copy link
Copy Markdown
Contributor

From triage: This is waiting on comments on the design doc to be addressed.

@stuartmorgan-g

Copy link
Copy Markdown
Contributor

From triage: @hearsilent It looks like the design doc has never been updated. Is this still something you are planning on moving forward?

Marking as a draft pending design doc approval.

@stuartmorgan-g
stuartmorgan-g marked this pull request as draft June 9, 2026 18:23
@Piinks

Piinks commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Thank you for your contribution. I'm going to close this PR for now since there are outstanding comments, just to get this off our PR review queue. Please don't hesitate to re-open or submit a new PR if you have the time to address the review comments. Thanks!

@Piinks Piinks closed this Aug 10, 2026
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.

[google_maps_flutter] Expose an "onTilesLoaded" to notify when all tiles have been fully rendered.

8 participants