diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index ef5c348d68b7..1376a60f3764 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.11.0 +* Adds map mounted check to google map controller map functions. * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. ## 2.10.0 diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart index d44aa4d341e2..22af4e553663 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart @@ -593,6 +593,36 @@ void runTests() { final String? error = await controller.getStyleError(); expect(error, isNotNull); }); + + testWidgets('testMapStateException', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + await pumpMap( + tester, + GoogleMap( + initialCameraPosition: kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + final GoogleMapController controller = await controllerCompleter.future; + + final double zoomLevel = await controller.getZoomLevel(); + expect(zoomLevel > 0, true); + + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + try { + await controller.getZoomLevel(); + fail('expected MapStateException'); + } on MapStateException catch (e) { + expect(e.message.isNotEmpty, true); + } + }); } /// Repeatedly checks an asynchronous value against a test condition. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 0a949e262bbe..f27708fe7404 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -33,3 +33,17 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_android: + { + path: ../../../../packages/google_maps_flutter/google_maps_flutter_android, + }, + google_maps_flutter_ios: + { + path: ../../../../packages/google_maps_flutter/google_maps_flutter_ios, + }, + } diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 5dd1cfdfd2ad..c8171d3f9550 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -6,6 +6,19 @@ part of '../google_maps_flutter.dart'; +/// Exception for map state errors when calling a function +/// of the map when it's not mounted. +class MapStateException implements Exception { + /// Creates a new instance of [MapStateException]. + MapStateException(this.message); + + /// Error message describing the exception. + final String message; + + @override + String toString() => 'MapStateException: $message'; +} + /// Controller for a single GoogleMap instance running on the host platform. class GoogleMapController { GoogleMapController._( @@ -181,8 +194,13 @@ class GoogleMapController { /// in-memory cache of tiles. If you want to cache tiles for longer, you /// should implement an on-disk cache. Future clearTileCache(TileOverlayId tileOverlayId) async { - return GoogleMapsFlutterPlatform.instance - .clearTileCache(tileOverlayId, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .clearTileCache(tileOverlayId, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method clearTileCache on an unmounted GoogleMap instance.'); + } } /// Starts an animated change of the map camera position. @@ -190,8 +208,13 @@ class GoogleMapController { /// The returned [Future] completes after the change has been started on the /// platform side. Future animateCamera(CameraUpdate cameraUpdate) { - return GoogleMapsFlutterPlatform.instance - .animateCamera(cameraUpdate, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .animateCamera(cameraUpdate, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method animateCamera on an unmounted GoogleMap instance.'); + } } /// Changes the map camera position. @@ -199,8 +222,13 @@ class GoogleMapController { /// The returned [Future] completes after the change has been made on the /// platform side. Future moveCamera(CameraUpdate cameraUpdate) { - return GoogleMapsFlutterPlatform.instance - .moveCamera(cameraUpdate, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .moveCamera(cameraUpdate, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method moveCamera on an unmounted GoogleMap instance.'); + } } /// Sets the styling of the base map. @@ -218,18 +246,33 @@ class GoogleMapController { /// style reference for more information regarding the supported styles. @Deprecated('Use GoogleMap.style instead.') Future setMapStyle(String? mapStyle) { - return GoogleMapsFlutterPlatform.instance - .setMapStyle(mapStyle, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .setMapStyle(mapStyle, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method setMapStyle on an unmounted GoogleMap instance.'); + } } /// Returns the last style error, if any. Future getStyleError() { - return GoogleMapsFlutterPlatform.instance.getStyleError(mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance.getStyleError(mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method getStyleError on an unmounted GoogleMap instance.'); + } } /// Return [LatLngBounds] defining the region that is visible in a map. Future getVisibleRegion() { - return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method getVisibleRegion on an unmounted GoogleMap instance.'); + } } /// Return [ScreenCoordinate] of the [LatLng] in the current map view. @@ -238,8 +281,13 @@ class GoogleMapController { /// Screen location is in screen pixels (not display pixels) with respect to the top left corner /// of the map, not necessarily of the whole screen. Future getScreenCoordinate(LatLng latLng) { - return GoogleMapsFlutterPlatform.instance - .getScreenCoordinate(latLng, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .getScreenCoordinate(latLng, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method getScreenCoordinate on an unmounted GoogleMap instance.'); + } } /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. @@ -247,8 +295,13 @@ class GoogleMapController { /// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen /// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen. Future getLatLng(ScreenCoordinate screenCoordinate) { - return GoogleMapsFlutterPlatform.instance - .getLatLng(screenCoordinate, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .getLatLng(screenCoordinate, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method getLatLng on an unmounted GoogleMap instance.'); + } } /// Programmatically show the Info Window for a [Marker]. @@ -260,8 +313,13 @@ class GoogleMapController { /// * [hideMarkerInfoWindow] to hide the Info Window. /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future showMarkerInfoWindow(MarkerId markerId) { - return GoogleMapsFlutterPlatform.instance - .showMarkerInfoWindow(markerId, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .showMarkerInfoWindow(markerId, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method showMarkerInfoWindow on an unmounted GoogleMap instance.'); + } } /// Programmatically hide the Info Window for a [Marker]. @@ -272,9 +330,15 @@ class GoogleMapController { /// * See also: /// * [showMarkerInfoWindow] to show the Info Window. /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. + /// Future hideMarkerInfoWindow(MarkerId markerId) { - return GoogleMapsFlutterPlatform.instance - .hideMarkerInfoWindow(markerId, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .hideMarkerInfoWindow(markerId, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method hideMarkerInfoWindow on an unmounted GoogleMap instance.'); + } } /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. @@ -286,18 +350,33 @@ class GoogleMapController { /// * [showMarkerInfoWindow] to show the Info Window. /// * [hideMarkerInfoWindow] to hide the Info Window. Future isMarkerInfoWindowShown(MarkerId markerId) { - return GoogleMapsFlutterPlatform.instance - .isMarkerInfoWindowShown(markerId, mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance + .isMarkerInfoWindowShown(markerId, mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method isMarkerInfoWindowShown on an unmounted GoogleMap instance.'); + } } /// Returns the current zoom level of the map Future getZoomLevel() { - return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method getZoomLevel on an unmounted GoogleMap instance.'); + } } /// Returns the image bytes of the map Future takeSnapshot() { - return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId); + if (_googleMapState.mounted) { + return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId); + } else { + throw MapStateException( + 'Cannot call method takeSnapshot on an unmounted GoogleMap instance.'); + } } /// Disposes of the platform resources diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index 888195ca36d2..6b5bfd4ccad6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -360,6 +360,7 @@ class _GoogleMapState extends State { circles: widget.circles, clusterManagers: widget.clusterManagers, heatmaps: widget.heatmaps, + tileOverlays: widget.tileOverlays, ), mapConfiguration: _mapConfiguration, ); @@ -469,7 +470,6 @@ class _GoogleMapState extends State { this, ); _controller.complete(controller); - unawaited(_updateTileOverlays()); final MapCreatedCallback? onMapCreated = widget.onMapCreated; if (onMapCreated != null) { onMapCreated(controller); diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 6afe27288aaf..41716cf8f708 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 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/packages/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.10.0 +version: 2.11.0 environment: sdk: ^3.4.0 @@ -41,3 +41,15 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + { + google_maps_flutter_android: + { + path: ../../../packages/google_maps_flutter/google_maps_flutter_android, + }, + google_maps_flutter_ios: + { path: ../../../packages/google_maps_flutter/google_maps_flutter_ios }, + } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 9276a7dbda30..50532e3382d8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -579,4 +582,41 @@ void main() { expect(map.mapConfiguration.style, ''); }); + + testWidgets('testMapStateException', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: + const CameraPosition(target: LatLng(10.0, 15.0)), + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + final GoogleMapController controller = await controllerCompleter.future; + + try { + await controller.getZoomLevel(); + } on MapStateException { + fail('should not throw MapStateException'); + } + + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + try { + await controller.getZoomLevel(); + fail('expected MapStateException'); + } on MapStateException catch (e) { + expect(e.message.isNotEmpty, true); + } + }); } 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 e18bd40228cf..7e416c8c895e 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.14.13 + +* Minor fix for managing tile overlays. + ## 2.14.12 * Updates androidx.annotation:annotation to 1.9.1. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index dd19bed86468..2c12b270d3be 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -133,7 +133,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { @override void dispose({required int mapId}) { - // Noop! + _tileOverlays.remove(mapId); } // The controller we need to broadcast the different events coming @@ -529,6 +529,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { .toList(), ); + if (_tileOverlays[creationId] == null) { + // Initialize the tile overlays for the mapId. + _tileOverlays[creationId] = keyTileOverlayId(mapObjects.tileOverlays); + } + const String viewType = 'plugins.flutter.dev/google_maps_android'; if (useAndroidViewSurface) { return PlatformViewLink( 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 19db56e19133..353f8e028583 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.14.12 +version: 2.14.13 environment: sdk: ^3.5.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md index b09f2f375b20..db6f5af53935 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.14.0 +* Minor fix for managing tile overlays. * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. ## 2.13.2 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart index 89c6b90ddc60..5ce36dcc68cb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -119,7 +119,7 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { @override void dispose({required int mapId}) { - // Noop! + _tileOverlays.remove(mapId); } // The controller we need to broadcast the different events coming @@ -471,6 +471,11 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { .toList(), ); + if (_tileOverlays[creationId] == null) { + // Initialize the tile overlays for the mapId. + _tileOverlays[creationId] = keyTileOverlayId(mapObjects.tileOverlays); + } + return UiKitView( viewType: 'plugins.flutter.dev/google_maps_ios', onPlatformViewCreated: onPlatformViewCreated, diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml index 2020ec8940ef..08b76f1cb18f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_ios description: iOS implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.13.2 +version: 2.14.0 environment: sdk: ^3.4.0