From 5aad9238feaa7c961a25f0943eb886cdc9456aa3 Mon Sep 17 00:00:00 2001 From: Eli Geller Date: Mon, 26 Jan 2026 11:14:35 -0500 Subject: [PATCH 1/6] batch cluster changes --- .../google_maps_flutter_android/CHANGELOG.md | 4 + .../googlemaps/ClusterManagersController.java | 18 +++ .../plugins/googlemaps/MarkersController.java | 77 +++++++++++- .../googlemaps/MarkersControllerTest.java | 111 ++++++++++++++++-- 4 files changed, 199 insertions(+), 11 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index fed644c5be91..c2d2d7c0d2c0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -10,6 +10,10 @@ * Adds support for advanced markers. +## 2.18.13 + +* Batches clustered marker add/remove operations to avoid redundant re-rendering. + ## 2.18.12 * Bumps com.google.maps.android:android-maps-utils from 3.20.1 to 4.0.0. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java index 272ae3b71b41..e6303b2a382d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java @@ -160,6 +160,15 @@ public void addItem(MarkerBuilder item) { } } + /** Adds multiple items to the ClusterManager with the given ID. */ + public void addItems(String clusterManagerId, List items) { + ClusterManager clusterManager = clusterManagerIdToManager.get(clusterManagerId); + if (clusterManager != null) { + clusterManager.addItems(items); + clusterManager.cluster(); + } + } + /** Removes item from the ClusterManager it belongs to. */ public void removeItem(MarkerBuilder item) { ClusterManager clusterManager = @@ -170,6 +179,15 @@ public void removeItem(MarkerBuilder item) { } } + /** Removes multiple items from the ClusterManager with the given ID. */ + public void removeItems(String clusterManagerId, List items) { + ClusterManager clusterManager = clusterManagerIdToManager.get(clusterManagerId); + if (clusterManager != null) { + clusterManager.removeItems(items); + clusterManager.cluster(); + } + } + /** Called when ClusterRenderer has rendered new visible marker to the map. */ void onClusterItemRendered(@NonNull MarkerBuilder item, @NonNull Marker marker) { // If map is being disposed, clusterItemRenderedListener might have been cleared and diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 8d60d1fae226..4989cfa4312a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -12,8 +12,10 @@ import com.google.maps.android.collections.MarkerManager; import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi; import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; class MarkersController { @@ -51,8 +53,38 @@ void setCollection(MarkerManager.Collection markerCollection) { } void addMarkers(@NonNull List markersToAdd) { + // Group markers by cluster manager ID for batch operations + Map> markersByCluster = new HashMap<>(); + List nonClusteredMarkers = new ArrayList<>(); + for (Messages.PlatformMarker markerToAdd : markersToAdd) { - addMarker(markerToAdd); + String markerId = markerToAdd.getMarkerId(); + String clusterManagerId = markerToAdd.getClusterManagerId(); + MarkerBuilder markerBuilder = new MarkerBuilder(markerId, clusterManagerId); + Convert.interpretMarkerOptions( + markerToAdd, markerBuilder, assetManager, density, bitmapDescriptorFactoryWrapper); + + // Store marker builder for future marker rebuilds when used under clusters. + markerIdToMarkerBuilder.put(markerId, markerBuilder); + + if (clusterManagerId == null) { + nonClusteredMarkers.add(markerBuilder); + } else { + if (!markersByCluster.containsKey(clusterManagerId)) { + markersByCluster.put(clusterManagerId, new ArrayList<>()); + } + markersByCluster.get(clusterManagerId).add(markerBuilder); + } + } + + // Add non-clustered markers to the collection + for (MarkerBuilder markerBuilder : nonClusteredMarkers) { + addMarkerToCollection(markerBuilder.markerId(), markerBuilder); + } + + // Batch add clustered markers + for (Map.Entry> entry : markersByCluster.entrySet()) { + clusterManagersController.addItems(entry.getKey(), entry.getValue()); } } @@ -63,8 +95,49 @@ void changeMarkers(@NonNull List markersToChange) { } void removeMarkers(@NonNull List markerIdsToRemove) { + // Group markers by cluster manager ID for batch operations + Map> markersByCluster = new HashMap<>(); + List nonClusteredControllers = new ArrayList<>(); + for (String markerId : markerIdsToRemove) { - removeMarker(markerId); + final MarkerBuilder markerBuilder = markerIdToMarkerBuilder.get(markerId); + if (markerBuilder == null) { + continue; + } + + final String clusterManagerId = markerBuilder.clusterManagerId(); + if (clusterManagerId != null) { + if (!markersByCluster.containsKey(clusterManagerId)) { + markersByCluster.put(clusterManagerId, new ArrayList<>()); + } + markersByCluster.get(clusterManagerId).add(markerBuilder); + } else { + final MarkerController markerController = markerIdToController.get(markerId); + if (markerController != null) { + nonClusteredControllers.add(markerController); + } + } + } + + // Batch remove clustered markers + for (Map.Entry> entry : markersByCluster.entrySet()) { + clusterManagersController.removeItems(entry.getKey(), entry.getValue()); + } + + // Remove non-clustered markers from the collection + for (MarkerController markerController : nonClusteredControllers) { + if (this.markerCollection != null) { + markerController.removeFromCollection(markerCollection); + } + } + + // Clean up all marker references + for (String markerId : markerIdsToRemove) { + markerIdToMarkerBuilder.remove(markerId); + final MarkerController markerController = markerIdToController.remove(markerId); + if (markerController != null) { + googleMapsMarkerIdToDartMarkerId.remove(markerController.getGoogleMapsMarkerId()); + } } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java index e407e61fd6d1..ae814c189a29 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -35,7 +35,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -207,15 +206,27 @@ public void controller_AddChangeAndRemoveMarkerWithClusterManagerId() { when(marker.getId()).thenReturn(googleMarkerId); - // Add marker and capture the markerBuilder + // Store reference to verify later, since markerIdToMarkerBuilder is private + final MarkerBuilder[] addedMarkerBuilder = new MarkerBuilder[1]; + + // Add marker and verify addItems is called with correct parameters controller.addMarkers(Collections.singletonList(builder.build())); - ArgumentCaptor captor = ArgumentCaptor.forClass(MarkerBuilder.class); - Mockito.verify(clusterManagersController, times(1)).addItem(captor.capture()); - MarkerBuilder capturedMarkerBuilder = captor.getValue(); - assertEquals(clusterManagerId, capturedMarkerBuilder.clusterManagerId()); + Mockito.verify(clusterManagersController, times(1)) + .addItems( + eq(clusterManagerId), + Mockito.argThat( + markerBuilders -> { + if (markerBuilders.size() == 1 + && markerBuilders.get(0).clusterManagerId().equals(clusterManagerId)) { + // Store reference for later use in onClusterItemRendered + addedMarkerBuilder[0] = markerBuilders.get(0); + return true; + } + return false; + })); // clusterManagersController calls onClusterItemRendered with created marker. - controller.onClusterItemRendered(capturedMarkerBuilder, marker); + controller.onClusterItemRendered(addedMarkerBuilder[0], marker); // Change marker to test that markerController is created and the marker can be updated final LatLng latLng2 = new LatLng(3.3, 4.4); @@ -234,9 +245,12 @@ public void controller_AddChangeAndRemoveMarkerWithClusterManagerId() { controller.removeMarkers(Collections.singletonList(googleMarkerId)); Mockito.verify(clusterManagersController, times(1)) - .removeItem( + .removeItems( + eq(clusterManagerId), Mockito.argThat( - markerBuilder -> markerBuilder.clusterManagerId().equals(clusterManagerId))); + markerBuilders -> + markerBuilders.size() == 1 + && markerBuilders.get(0).clusterManagerId().equals(clusterManagerId))); } @Test @@ -310,4 +324,83 @@ public void markerBuilder_setCollisionBehavior() { markerOptions = markerBuilder.build(); Assert.assertEquals(MarkerOptions.class, markerOptions.getClass()); } + + @Test + public void controller_BatchAddMultipleMarkersWithClusterManagerId() { + final String clusterManagerId = "cm123"; + + // Create multiple markers with the same cluster manager + final List markers = new java.util.ArrayList<>(); + for (int i = 0; i < 5; i++) { + final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); + builder + .setMarkerId("marker" + i) + .setClusterManagerId(clusterManagerId) + .setPosition( + new Messages.PlatformLatLng.Builder() + .setLatitude(1.0 + i) + .setLongitude(2.0 + i) + .build()); + markers.add(builder.build()); + } + + // Add all markers in one batch + controller.addMarkers(markers); + + // Verify addItems is called exactly once with all 5 markers + Mockito.verify(clusterManagersController, times(1)) + .addItems( + eq(clusterManagerId), + Mockito.argThat( + markerBuilders -> + markerBuilders.size() == 5 + && markerBuilders + .stream() + .allMatch(mb -> mb.clusterManagerId().equals(clusterManagerId)))); + + // Verify addItem is never called (we're using batch operation) + Mockito.verify(clusterManagersController, times(0)).addItem(any()); + } + + @Test + public void controller_BatchRemoveMultipleMarkersWithClusterManagerId() { + final String clusterManagerId = "cm123"; + + // First add markers + final List markers = new java.util.ArrayList<>(); + final List markerIds = new java.util.ArrayList<>(); + for (int i = 0; i < 5; i++) { + String markerId = "marker" + i; + markerIds.add(markerId); + final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); + builder + .setMarkerId(markerId) + .setClusterManagerId(clusterManagerId) + .setPosition( + new Messages.PlatformLatLng.Builder() + .setLatitude(1.0 + i) + .setLongitude(2.0 + i) + .build()); + markers.add(builder.build()); + } + + controller.addMarkers(markers); + + // Remove all markers in one batch + controller.removeMarkers(markerIds); + + // Verify removeItems is called exactly once with all 5 markers + Mockito.verify(clusterManagersController, times(1)) + .removeItems( + eq(clusterManagerId), + Mockito.argThat( + markerBuilders -> + markerBuilders.size() == 5 + && markerBuilders + .stream() + .allMatch(mb -> mb.clusterManagerId().equals(clusterManagerId)))); + + // Verify removeItem is never called (we're using batch operation) + Mockito.verify(clusterManagersController, times(0)).removeItem(any()); + } } From 8df77eff8093fe05e3e8bb4719890b9200b535aa Mon Sep 17 00:00:00 2001 From: Eli Geller Date: Mon, 26 Jan 2026 15:20:04 -0500 Subject: [PATCH 2/6] batch logic for changeMarker --- .../google_maps_flutter_android/CHANGELOG.md | 3 +- .../plugins/googlemaps/MarkersController.java | 76 ++++++++++++++++++- .../googlemaps/MarkersControllerTest.java | 67 ++++++++++++++++ 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index c2d2d7c0d2c0..0a61c243245d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -39,6 +39,7 @@ * Replaces internal use of deprecated methods. ## 2.18.6 + * Bumps com.android.tools.build:gradle from 8.12.1 to 8.13.1. ## 2.18.5 @@ -64,7 +65,7 @@ ## 2.18.0 -* Adds support for warming up the Google Maps SDK +* Adds support for warming up the Google Maps SDK via `GoogleMapsFlutterAndroid.warmup()`. ## 2.17.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 4989cfa4312a..9cb3903dc45f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -89,8 +89,82 @@ void addMarkers(@NonNull List markersToAdd) { } void changeMarkers(@NonNull List markersToChange) { + // Collect markers that need cluster manager changes for batch processing + Map> markersToAddByCluster = new HashMap<>(); + Map> markersToRemoveByCluster = new HashMap<>(); + for (Messages.PlatformMarker markerToChange : markersToChange) { - changeMarker(markerToChange); + String markerId = markerToChange.getMarkerId(); + MarkerBuilder markerBuilder = markerIdToMarkerBuilder.get(markerId); + if (markerBuilder == null) { + continue; + } + + String clusterManagerId = markerToChange.getClusterManagerId(); + String oldClusterManagerId = markerBuilder.clusterManagerId(); + + // If the cluster ID on the updated marker has changed, collect for batch processing + if (!(Objects.equals(clusterManagerId, oldClusterManagerId))) { + // Remove from old cluster manager + if (oldClusterManagerId != null) { + if (!markersToRemoveByCluster.containsKey(oldClusterManagerId)) { + markersToRemoveByCluster.put(oldClusterManagerId, new ArrayList<>()); + } + markersToRemoveByCluster.get(oldClusterManagerId).add(markerBuilder); + } + + // Prepare new marker for addition + MarkerBuilder newMarkerBuilder = new MarkerBuilder(markerId, clusterManagerId); + Convert.interpretMarkerOptions( + markerToChange, + newMarkerBuilder, + assetManager, + density, + bitmapDescriptorFactoryWrapper); + markerIdToMarkerBuilder.put(markerId, newMarkerBuilder); + + if (clusterManagerId != null) { + if (!markersToAddByCluster.containsKey(clusterManagerId)) { + markersToAddByCluster.put(clusterManagerId, new ArrayList<>()); + } + markersToAddByCluster.get(clusterManagerId).add(newMarkerBuilder); + } else { + // Add to map immediately if not clustered + addMarkerToCollection(markerId, newMarkerBuilder); + } + + // Clean up old marker controller if it's not clustered + if (oldClusterManagerId == null) { + MarkerController oldController = markerIdToController.remove(markerId); + if (oldController != null && markerCollection != null) { + oldController.removeFromCollection(markerCollection); + googleMapsMarkerIdToDartMarkerId.remove(oldController.getGoogleMapsMarkerId()); + } + } + } else { + // Update existing marker in place + Convert.interpretMarkerOptions( + markerToChange, markerBuilder, assetManager, density, bitmapDescriptorFactoryWrapper); + MarkerController markerController = markerIdToController.get(markerId); + if (markerController != null) { + Convert.interpretMarkerOptions( + markerToChange, + markerController, + assetManager, + density, + bitmapDescriptorFactoryWrapper); + } + } + } + + // Batch remove from cluster managers + for (Map.Entry> entry : markersToRemoveByCluster.entrySet()) { + clusterManagersController.removeItems(entry.getKey(), entry.getValue()); + } + + // Batch add to cluster managers + for (Map.Entry> entry : markersToAddByCluster.entrySet()) { + clusterManagersController.addItems(entry.getKey(), entry.getValue()); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java index ae814c189a29..b7089f59d0ce 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -403,4 +403,71 @@ public void controller_BatchRemoveMultipleMarkersWithClusterManagerId() { // Verify removeItem is never called (we're using batch operation) Mockito.verify(clusterManagersController, times(0)).removeItem(any()); } + + @Test + public void controller_BatchChangeMarkersWithClusterManagerChange() { + final String clusterManagerId1 = "cm123"; + final String clusterManagerId2 = "cm456"; + + // First add markers to cluster manager 1 + final List initialMarkers = new java.util.ArrayList<>(); + for (int i = 0; i < 5; i++) { + final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); + builder + .setMarkerId("marker" + i) + .setClusterManagerId(clusterManagerId1) + .setPosition( + new Messages.PlatformLatLng.Builder() + .setLatitude(1.0 + i) + .setLongitude(2.0 + i) + .build()); + initialMarkers.add(builder.build()); + } + controller.addMarkers(initialMarkers); + + // Reset mock to clear invocation counts + Mockito.reset(clusterManagersController); + + // Now change all markers to cluster manager 2 + final List changedMarkers = new java.util.ArrayList<>(); + for (int i = 0; i < 5; i++) { + final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); + builder + .setMarkerId("marker" + i) + .setClusterManagerId(clusterManagerId2) // Different cluster manager + .setPosition( + new Messages.PlatformLatLng.Builder() + .setLatitude(3.0 + i) + .setLongitude(4.0 + i) + .build()); + changedMarkers.add(builder.build()); + } + controller.changeMarkers(changedMarkers); + + // Verify removeItems is called exactly once for cluster manager 1 with all 5 markers + Mockito.verify(clusterManagersController, times(1)) + .removeItems( + eq(clusterManagerId1), + Mockito.argThat( + markerBuilders -> + markerBuilders.size() == 5 + && markerBuilders + .stream() + .allMatch(mb -> mb.clusterManagerId().equals(clusterManagerId1)))); + + // Verify addItems is called exactly once for cluster manager 2 with all 5 markers + Mockito.verify(clusterManagersController, times(1)) + .addItems( + eq(clusterManagerId2), + Mockito.argThat( + markerBuilders -> + markerBuilders.size() == 5 + && markerBuilders + .stream() + .allMatch(mb -> mb.clusterManagerId().equals(clusterManagerId2)))); + + // Verify individual operations are never called (we're using batch operations) + Mockito.verify(clusterManagersController, times(0)).addItem(any()); + Mockito.verify(clusterManagersController, times(0)).removeItem(any()); + } } From 313955100b5b93968e93b97db57b88a2d91ce1f6 Mon Sep 17 00:00:00 2001 From: Eli Geller Date: Sun, 1 Feb 2026 19:16:01 -0500 Subject: [PATCH 3/6] Use computeIfAbsent for more concise map operations --- .../plugins/googlemaps/MarkersController.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 9cb3903dc45f..fd71f66a3b30 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -70,10 +70,7 @@ void addMarkers(@NonNull List markersToAdd) { if (clusterManagerId == null) { nonClusteredMarkers.add(markerBuilder); } else { - if (!markersByCluster.containsKey(clusterManagerId)) { - markersByCluster.put(clusterManagerId, new ArrayList<>()); - } - markersByCluster.get(clusterManagerId).add(markerBuilder); + markersByCluster.computeIfAbsent(clusterManagerId, k -> new ArrayList<>()).add(markerBuilder); } } @@ -107,10 +104,7 @@ void changeMarkers(@NonNull List markersToChange) { if (!(Objects.equals(clusterManagerId, oldClusterManagerId))) { // Remove from old cluster manager if (oldClusterManagerId != null) { - if (!markersToRemoveByCluster.containsKey(oldClusterManagerId)) { - markersToRemoveByCluster.put(oldClusterManagerId, new ArrayList<>()); - } - markersToRemoveByCluster.get(oldClusterManagerId).add(markerBuilder); + markersToRemoveByCluster.computeIfAbsent(oldClusterManagerId, k -> new ArrayList<>()).add(markerBuilder); } // Prepare new marker for addition @@ -124,10 +118,7 @@ void changeMarkers(@NonNull List markersToChange) { markerIdToMarkerBuilder.put(markerId, newMarkerBuilder); if (clusterManagerId != null) { - if (!markersToAddByCluster.containsKey(clusterManagerId)) { - markersToAddByCluster.put(clusterManagerId, new ArrayList<>()); - } - markersToAddByCluster.get(clusterManagerId).add(newMarkerBuilder); + markersToAddByCluster.computeIfAbsent(clusterManagerId, k -> new ArrayList<>()).add(newMarkerBuilder); } else { // Add to map immediately if not clustered addMarkerToCollection(markerId, newMarkerBuilder); @@ -181,10 +172,7 @@ void removeMarkers(@NonNull List markerIdsToRemove) { final String clusterManagerId = markerBuilder.clusterManagerId(); if (clusterManagerId != null) { - if (!markersByCluster.containsKey(clusterManagerId)) { - markersByCluster.put(clusterManagerId, new ArrayList<>()); - } - markersByCluster.get(clusterManagerId).add(markerBuilder); + markersByCluster.computeIfAbsent(clusterManagerId, k -> new ArrayList<>()).add(markerBuilder); } else { final MarkerController markerController = markerIdToController.get(markerId); if (markerController != null) { From bd708940332e7ee246930cfdac17cf9c77ef0f32 Mon Sep 17 00:00:00 2001 From: Eli Geller Date: Tue, 3 Feb 2026 14:48:49 -0500 Subject: [PATCH 4/6] format tool --- .../plugins/googlemaps/MarkersController.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index fd71f66a3b30..53b6bddbfe70 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -70,7 +70,9 @@ void addMarkers(@NonNull List markersToAdd) { if (clusterManagerId == null) { nonClusteredMarkers.add(markerBuilder); } else { - markersByCluster.computeIfAbsent(clusterManagerId, k -> new ArrayList<>()).add(markerBuilder); + markersByCluster + .computeIfAbsent(clusterManagerId, k -> new ArrayList<>()) + .add(markerBuilder); } } @@ -104,7 +106,9 @@ void changeMarkers(@NonNull List markersToChange) { if (!(Objects.equals(clusterManagerId, oldClusterManagerId))) { // Remove from old cluster manager if (oldClusterManagerId != null) { - markersToRemoveByCluster.computeIfAbsent(oldClusterManagerId, k -> new ArrayList<>()).add(markerBuilder); + markersToRemoveByCluster + .computeIfAbsent(oldClusterManagerId, k -> new ArrayList<>()) + .add(markerBuilder); } // Prepare new marker for addition @@ -118,7 +122,9 @@ void changeMarkers(@NonNull List markersToChange) { markerIdToMarkerBuilder.put(markerId, newMarkerBuilder); if (clusterManagerId != null) { - markersToAddByCluster.computeIfAbsent(clusterManagerId, k -> new ArrayList<>()).add(newMarkerBuilder); + markersToAddByCluster + .computeIfAbsent(clusterManagerId, k -> new ArrayList<>()) + .add(newMarkerBuilder); } else { // Add to map immediately if not clustered addMarkerToCollection(markerId, newMarkerBuilder); @@ -172,7 +178,9 @@ void removeMarkers(@NonNull List markerIdsToRemove) { final String clusterManagerId = markerBuilder.clusterManagerId(); if (clusterManagerId != null) { - markersByCluster.computeIfAbsent(clusterManagerId, k -> new ArrayList<>()).add(markerBuilder); + markersByCluster + .computeIfAbsent(clusterManagerId, k -> new ArrayList<>()) + .add(markerBuilder); } else { final MarkerController markerController = markerIdToController.get(markerId); if (markerController != null) { From b0437949fc7f3a4fe29070d28da6d84f54c4292b Mon Sep 17 00:00:00 2001 From: Eli Geller Date: Tue, 24 Feb 2026 21:27:35 -0500 Subject: [PATCH 5/6] Rebase onto upstream v2.19.2 + address review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Incorporate upstream advanced markers support (v2.19.0–2.19.2): PlatformMarkerType, MarkerClusterRenderer/AdvancedMarkerClusterRenderer, 3-arg MarkerBuilder constructor, isAdvancedMarkersAvailable(), etc. - Keep batch add/remove logic in ClusterManagersController and MarkersController - Add @NonNull to addItems/removeItems parameters (r2823177045, r2823177715) - Add controller_ChangeMarkerInPlace test (r2823210221) - Bump version to 2.19.3 --- .../google_maps_flutter_android/CHANGELOG.md | 8 +-- .../googlemaps/ClusterManagersController.java | 4 +- .../plugins/googlemaps/MarkersController.java | 4 +- .../googlemaps/MarkersControllerTest.java | 58 +++++++++++++++++-- .../google_maps_flutter_android/pubspec.yaml | 2 +- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 0a61c243245d..51d54ce382e3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.19.3 + +* Batches clustered marker add/remove operations to avoid redundant re-rendering. + ## 2.19.2 * Bump com.google.maps.android:android-maps-utils from 4.0.0 to 4.1.0. @@ -10,10 +14,6 @@ * Adds support for advanced markers. -## 2.18.13 - -* Batches clustered marker add/remove operations to avoid redundant re-rendering. - ## 2.18.12 * Bumps com.google.maps.android:android-maps-utils from 3.20.1 to 4.0.0. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java index e6303b2a382d..0b28e6a485ec 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java @@ -161,7 +161,7 @@ public void addItem(MarkerBuilder item) { } /** Adds multiple items to the ClusterManager with the given ID. */ - public void addItems(String clusterManagerId, List items) { + public void addItems(String clusterManagerId, @NonNull List items) { ClusterManager clusterManager = clusterManagerIdToManager.get(clusterManagerId); if (clusterManager != null) { clusterManager.addItems(items); @@ -180,7 +180,7 @@ public void removeItem(MarkerBuilder item) { } /** Removes multiple items from the ClusterManager with the given ID. */ - public void removeItems(String clusterManagerId, List items) { + public void removeItems(String clusterManagerId, @NonNull List items) { ClusterManager clusterManager = clusterManagerIdToManager.get(clusterManagerId); if (clusterManager != null) { clusterManager.removeItems(items); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 53b6bddbfe70..7f95e5d44efe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -60,7 +60,7 @@ void addMarkers(@NonNull List markersToAdd) { for (Messages.PlatformMarker markerToAdd : markersToAdd) { String markerId = markerToAdd.getMarkerId(); String clusterManagerId = markerToAdd.getClusterManagerId(); - MarkerBuilder markerBuilder = new MarkerBuilder(markerId, clusterManagerId); + MarkerBuilder markerBuilder = new MarkerBuilder(markerId, clusterManagerId, markerType); Convert.interpretMarkerOptions( markerToAdd, markerBuilder, assetManager, density, bitmapDescriptorFactoryWrapper); @@ -112,7 +112,7 @@ void changeMarkers(@NonNull List markersToChange) { } // Prepare new marker for addition - MarkerBuilder newMarkerBuilder = new MarkerBuilder(markerId, clusterManagerId); + MarkerBuilder newMarkerBuilder = new MarkerBuilder(markerId, clusterManagerId, markerType); Convert.interpretMarkerOptions( markerToChange, newMarkerBuilder, diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java index b7089f59d0ce..0d9c8ec6b37f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -28,6 +28,7 @@ import io.flutter.plugins.googlemaps.Messages.PlatformMarkerCollisionBehavior; import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.After; @@ -35,6 +36,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -330,7 +332,7 @@ public void controller_BatchAddMultipleMarkersWithClusterManagerId() { final String clusterManagerId = "cm123"; // Create multiple markers with the same cluster manager - final List markers = new java.util.ArrayList<>(); + final List markers = new ArrayList<>(); for (int i = 0; i < 5; i++) { final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); builder @@ -367,8 +369,8 @@ public void controller_BatchRemoveMultipleMarkersWithClusterManagerId() { final String clusterManagerId = "cm123"; // First add markers - final List markers = new java.util.ArrayList<>(); - final List markerIds = new java.util.ArrayList<>(); + final List markers = new ArrayList<>(); + final List markerIds = new ArrayList<>(); for (int i = 0; i < 5; i++) { String markerId = "marker" + i; markerIds.add(markerId); @@ -410,7 +412,7 @@ public void controller_BatchChangeMarkersWithClusterManagerChange() { final String clusterManagerId2 = "cm456"; // First add markers to cluster manager 1 - final List initialMarkers = new java.util.ArrayList<>(); + final List initialMarkers = new ArrayList<>(); for (int i = 0; i < 5; i++) { final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); builder @@ -429,7 +431,7 @@ public void controller_BatchChangeMarkersWithClusterManagerChange() { Mockito.reset(clusterManagersController); // Now change all markers to cluster manager 2 - final List changedMarkers = new java.util.ArrayList<>(); + final List changedMarkers = new ArrayList<>(); for (int i = 0; i < 5; i++) { final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); builder @@ -470,4 +472,50 @@ public void controller_BatchChangeMarkersWithClusterManagerChange() { Mockito.verify(clusterManagersController, times(0)).addItem(any()); Mockito.verify(clusterManagersController, times(0)).removeItem(any()); } + + @Test + public void controller_ChangeMarkerInPlace() { + final Marker marker = mock(Marker.class); + final String markerId = "marker1"; + final String clusterManagerId = "cm123"; + + when(marker.getId()).thenReturn(markerId); + + // Add a clustered marker + final Messages.PlatformMarker.Builder builder = defaultMarkerBuilder(); + builder + .setMarkerId(markerId) + .setClusterManagerId(clusterManagerId) + .setPosition( + new Messages.PlatformLatLng.Builder().setLatitude(1.0).setLongitude(2.0).build()); + controller.addMarkers(Collections.singletonList(builder.build())); + + // Capture the MarkerBuilder passed to addItems + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + Mockito.verify(clusterManagersController).addItems(eq(clusterManagerId), captor.capture()); + MarkerBuilder capturedMarkerBuilder = captor.getValue().get(0); + + // Simulate cluster render so markerController exists + controller.onClusterItemRendered(capturedMarkerBuilder, marker); + + // Reset to clear invocation counts + Mockito.reset(clusterManagersController); + + // Change marker in place (same clusterManagerId) + final LatLng newLatLng = new LatLng(3.0, 4.0); + builder.setPosition( + new Messages.PlatformLatLng.Builder() + .setLatitude(newLatLng.latitude) + .setLongitude(newLatLng.longitude) + .build()); + controller.changeMarkers(Collections.singletonList(builder.build())); + + // In-place update: marker position is updated directly + Mockito.verify(marker, times(1)).setPosition(newLatLng); + // No re-clustering needed + Mockito.verify(clusterManagersController, times(0)).addItems(any(), any()); + Mockito.verify(clusterManagersController, times(0)).removeItems(any(), any()); + } + } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index 2caebe121cde..817e60a672ae 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.19.2 +version: 2.19.3 environment: sdk: ^3.9.0 From 7f78dad1dad10cd572e31f43d2b7ba71a2d729a8 Mon Sep 17 00:00:00 2001 From: Eli Geller Date: Sun, 1 Mar 2026 20:19:08 -0500 Subject: [PATCH 6/6] format --- .../io/flutter/plugins/googlemaps/MarkersControllerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java index 0d9c8ec6b37f..aba30f67ed02 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -517,5 +517,4 @@ public void controller_ChangeMarkerInPlace() { Mockito.verify(clusterManagersController, times(0)).addItems(any(), any()); Mockito.verify(clusterManagersController, times(0)).removeItems(any(), any()); } - }