Fix issue where map updates don't take effect in Flutter v3.0.0 (#5787)
diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
index 57d24e2..b4fb662 100644
--- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 2.1.6
+* Fixes issue in Flutter v3.0.0 where some updates to the map don't take effect on Android.
* Fixes iOS native unit tests on M1 devices.
* Minor fixes for new analysis options.
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
index 9b88103..2c2287c 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -12,9 +12,11 @@
import android.graphics.Point;
import android.os.Bundle;
import android.util.Log;
+import android.view.Choreographer;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
@@ -109,6 +111,11 @@
return mapView;
}
+ @VisibleForTesting
+ /*package*/ void setView(MapView view) {
+ mapView = view;
+ }
+
void init() {
lifecycleProvider.getLifecycle().addObserver(this);
mapView.getMapAsync(this);
@@ -126,6 +133,58 @@
return trackCameraPosition ? googleMap.getCameraPosition() : null;
}
+ private boolean loadedCallbackPending = false;
+
+ /**
+ * Invalidates the map view after the map has finished rendering.
+ *
+ * <p>gmscore GL renderer uses a {@link android.view.TextureView}. Android platform views that are
+ * displayed as a texture after Flutter v3.0.0. require that the view hierarchy is notified after
+ * all drawing operations have been flushed.
+ *
+ * <p>Since the GL renderer doesn't use standard Android views, and instead uses GL directly, we
+ * notify the view hierarchy by invalidating the view.
+ *
+ * <p>Unfortunately, when {@link GoogleMap.OnMapLoadedCallback} is fired, the texture may not have
+ * been updated yet.
+ *
+ * <p>To workaround this limitation, wait two frames. This ensures that at least the frame budget
+ * (16.66ms at 60hz) have passed since the drawing operation was issued.
+ */
+ private void invalidateMapIfNeeded() {
+ if (googleMap == null || loadedCallbackPending) {
+ return;
+ }
+ loadedCallbackPending = true;
+ googleMap.setOnMapLoadedCallback(
+ new GoogleMap.OnMapLoadedCallback() {
+ @Override
+ public void onMapLoaded() {
+ loadedCallbackPending = false;
+ postFrameCallback(
+ () -> {
+ postFrameCallback(
+ () -> {
+ if (mapView != null) {
+ mapView.invalidate();
+ }
+ });
+ });
+ }
+ });
+ }
+
+ private static void postFrameCallback(Runnable f) {
+ Choreographer.getInstance()
+ .postFrameCallback(
+ new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ f.run();
+ }
+ });
+ }
+
@Override
public void onMapReady(GoogleMap googleMap) {
this.googleMap = googleMap;
@@ -244,6 +303,7 @@
}
case "markers#update":
{
+ invalidateMapIfNeeded();
List<Object> markersToAdd = call.argument("markersToAdd");
markersController.addMarkers(markersToAdd);
List<Object> markersToChange = call.argument("markersToChange");
@@ -273,6 +333,7 @@
}
case "polygons#update":
{
+ invalidateMapIfNeeded();
List<Object> polygonsToAdd = call.argument("polygonsToAdd");
polygonsController.addPolygons(polygonsToAdd);
List<Object> polygonsToChange = call.argument("polygonsToChange");
@@ -284,6 +345,7 @@
}
case "polylines#update":
{
+ invalidateMapIfNeeded();
List<Object> polylinesToAdd = call.argument("polylinesToAdd");
polylinesController.addPolylines(polylinesToAdd);
List<Object> polylinesToChange = call.argument("polylinesToChange");
@@ -295,6 +357,7 @@
}
case "circles#update":
{
+ invalidateMapIfNeeded();
List<Object> circlesToAdd = call.argument("circlesToAdd");
circlesController.addCircles(circlesToAdd);
List<Object> circlesToChange = call.argument("circlesToChange");
@@ -374,12 +437,17 @@
}
case "map#setStyle":
{
- String mapStyle = (String) call.arguments;
+ invalidateMapIfNeeded();
boolean mapStyleSet;
- if (mapStyle == null) {
- mapStyleSet = googleMap.setMapStyle(null);
+ if (call.arguments instanceof String) {
+ String mapStyle = (String) call.arguments;
+ if (mapStyle == null) {
+ mapStyleSet = googleMap.setMapStyle(null);
+ } else {
+ mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle));
+ }
} else {
- mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle));
+ mapStyleSet = googleMap.setMapStyle(null);
}
ArrayList<Object> mapStyleResult = new ArrayList<>(2);
mapStyleResult.add(mapStyleSet);
@@ -392,6 +460,7 @@
}
case "tileOverlays#update":
{
+ invalidateMapIfNeeded();
List<Map<String, ?>> tileOverlaysToAdd = call.argument("tileOverlaysToAdd");
tileOverlaysController.addTileOverlays(tileOverlaysToAdd);
List<Map<String, ?>> tileOverlaysToChange = call.argument("tileOverlaysToChange");
@@ -403,6 +472,7 @@
}
case "tileOverlays#clearTileCache":
{
+ invalidateMapIfNeeded();
String tileOverlayId = call.argument("tileOverlayId");
tileOverlaysController.clearTileCache(tileOverlayId);
result.success(null);
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java
index 6bda085..d8082b5 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java
+++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java
@@ -6,16 +6,24 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import android.content.Context;
import android.os.Build;
import androidx.activity.ComponentActivity;
import androidx.test.core.app.ApplicationProvider;
import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.MapView;
import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
@@ -58,4 +66,83 @@
googleMapController.onDestroy(activity);
assertNull(googleMapController.getView());
}
+
+ @Test
+ public void InvalidateMapAfterMethodCalls() throws InterruptedException {
+ String[] methodsThatTriggerInvalidation = {
+ "markers#update",
+ "polygons#update",
+ "polylines#update",
+ "circles#update",
+ "map#setStyle",
+ "tileOverlays#update",
+ "tileOverlays#clearTileCache"
+ };
+
+ for (String methodName : methodsThatTriggerInvalidation) {
+ googleMapController =
+ new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null);
+ googleMapController.init();
+
+ mockGoogleMap = mock(GoogleMap.class);
+ googleMapController.onMapReady(mockGoogleMap);
+
+ MethodChannel.Result result = mock(MethodChannel.Result.class);
+ System.out.println(methodName);
+ googleMapController.onMethodCall(
+ new MethodCall(methodName, new HashMap<String, Object>()), result);
+
+ ArgumentCaptor<GoogleMap.OnMapLoadedCallback> argument =
+ ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class);
+ verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture());
+
+ MapView mapView = mock(MapView.class);
+ googleMapController.setView(mapView);
+
+ verify(mapView, never()).invalidate();
+ argument.getValue().onMapLoaded();
+ verify(mapView).invalidate();
+ }
+ }
+
+ @Test
+ public void InvalidateMapOnceAfterMethodCall() throws InterruptedException {
+ googleMapController.onMapReady(mockGoogleMap);
+
+ MethodChannel.Result result = mock(MethodChannel.Result.class);
+ googleMapController.onMethodCall(
+ new MethodCall("markers#update", new HashMap<String, Object>()), result);
+ googleMapController.onMethodCall(
+ new MethodCall("polygons#update", new HashMap<String, Object>()), result);
+
+ ArgumentCaptor<GoogleMap.OnMapLoadedCallback> argument =
+ ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class);
+ verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture());
+
+ MapView mapView = mock(MapView.class);
+ googleMapController.setView(mapView);
+
+ verify(mapView, never()).invalidate();
+ argument.getValue().onMapLoaded();
+ verify(mapView).invalidate();
+ }
+
+ @Test
+ public void MethodCalledAfterControllerIsDestroyed() throws InterruptedException {
+ googleMapController.onMapReady(mockGoogleMap);
+ MethodChannel.Result result = mock(MethodChannel.Result.class);
+ googleMapController.onMethodCall(
+ new MethodCall("markers#update", new HashMap<String, Object>()), result);
+
+ ArgumentCaptor<GoogleMap.OnMapLoadedCallback> argument =
+ ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class);
+ verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture());
+
+ MapView mapView = mock(MapView.class);
+ googleMapController.setView(mapView);
+ googleMapController.onDestroy(activity);
+
+ argument.getValue().onMapLoaded();
+ verify(mapView, never()).invalidate();
+ }
}
diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
index 831f3cc..59ee23d 100644
--- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
-version: 2.1.5
+version: 2.1.6
environment:
sdk: ">=2.14.0 <3.0.0"