From 320c1dcdbb8ad51d911c87e279800f50acb37924 Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Thu, 8 May 2025 22:59:56 +1200 Subject: [PATCH 1/4] Mount checks --- .../google_maps_flutter/CHANGELOG.md | 4 +++ .../lib/src/google_map.dart | 27 +++++++++++++++++++ .../google_maps_flutter/pubspec.yaml | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index edc28239419..67d07e8d789 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.12.3 + +* Fixes exception when dispose is called while asynchronous update from didUpdateWidget is executed + ## 2.12.2 * Fixes memory leak by disposing stream subscriptions in `GoogleMapController`. 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 76bcad6e26c..c8bc3ba1024 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 @@ -447,12 +447,18 @@ class _GoogleMapState extends State { return; } final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updateMapConfiguration(updates)); _mapConfiguration = newConfig; } Future _updateMarkers() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updateMarkers( MarkerUpdates.from(_markers.values.toSet(), widget.markers))); _markers = keyByMarkerId(widget.markers); @@ -460,6 +466,9 @@ class _GoogleMapState extends State { Future _updateClusterManagers() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updateClusterManagers(ClusterManagerUpdates.from( _clusterManagers.values.toSet(), widget.clusterManagers))); _clusterManagers = keyByClusterManagerId(widget.clusterManagers); @@ -467,6 +476,9 @@ class _GoogleMapState extends State { Future _updateGroundOverlays() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updateGroundOverlays(GroundOverlayUpdates.from( _groundOverlays.values.toSet(), widget.groundOverlays))); _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); @@ -474,6 +486,9 @@ class _GoogleMapState extends State { Future _updatePolygons() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updatePolygons( PolygonUpdates.from(_polygons.values.toSet(), widget.polygons))); _polygons = keyByPolygonId(widget.polygons); @@ -481,6 +496,9 @@ class _GoogleMapState extends State { Future _updatePolylines() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updatePolylines( PolylineUpdates.from(_polylines.values.toSet(), widget.polylines))); _polylines = keyByPolylineId(widget.polylines); @@ -488,6 +506,9 @@ class _GoogleMapState extends State { Future _updateCircles() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updateCircles( CircleUpdates.from(_circles.values.toSet(), widget.circles))); _circles = keyByCircleId(widget.circles); @@ -495,6 +516,9 @@ class _GoogleMapState extends State { Future _updateHeatmaps() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited( controller._updateHeatmaps( HeatmapUpdates.from(_heatmaps.values.toSet(), widget.heatmaps), @@ -505,6 +529,9 @@ class _GoogleMapState extends State { Future _updateTileOverlays() async { final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } unawaited(controller._updateTileOverlays(widget.tileOverlays)); } diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 974bbaa2101..c7126a047b4 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.12.2 +version: 2.12.3 environment: sdk: ^3.6.0 From 4b325f306e22908e18f7113fb3b6318a83fbac16 Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Thu, 8 May 2025 23:01:59 +1200 Subject: [PATCH 2/4] Single retrieval of controller --- .../lib/src/google_map.dart | 85 +++++++------------ 1 file changed, 30 insertions(+), 55 deletions(-) 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 c8bc3ba1024..53cb4869e79 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 @@ -429,96 +429,75 @@ class _GoogleMapState extends State { @override void didUpdateWidget(GoogleMap oldWidget) { super.didUpdateWidget(oldWidget); - _updateOptions(); - _updateClusterManagers(); - _updateMarkers(); - _updatePolygons(); - _updatePolylines(); - _updateCircles(); - _updateHeatmaps(); - _updateTileOverlays(); - _updateGroundOverlays(); + + _refreshStateFromWidget(); + } + + Future _refreshStateFromWidget() async { + final GoogleMapController controller = await _controller.future; + if (!mounted) { + return; + } + + _updateOptions(controller); + _updateClusterManagers(controller); + _updateMarkers(controller); + _updatePolygons(controller); + _updatePolylines(controller); + _updateCircles(controller); + _updateHeatmaps(controller); + _updateTileOverlays(controller); + _updateGroundOverlays(controller); } - Future _updateOptions() async { + void _updateOptions(GoogleMapController controller) { final MapConfiguration newConfig = _configurationFromMapWidget(widget); final MapConfiguration updates = newConfig.diffFrom(_mapConfiguration); if (updates.isEmpty) { return; } - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + unawaited(controller._updateMapConfiguration(updates)); _mapConfiguration = newConfig; } - Future _updateMarkers() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updateMarkers(GoogleMapController controller) { unawaited(controller._updateMarkers( MarkerUpdates.from(_markers.values.toSet(), widget.markers))); _markers = keyByMarkerId(widget.markers); } - Future _updateClusterManagers() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updateClusterManagers(GoogleMapController controller) { unawaited(controller._updateClusterManagers(ClusterManagerUpdates.from( _clusterManagers.values.toSet(), widget.clusterManagers))); _clusterManagers = keyByClusterManagerId(widget.clusterManagers); } - Future _updateGroundOverlays() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updateGroundOverlays(GoogleMapController controller) { unawaited(controller._updateGroundOverlays(GroundOverlayUpdates.from( _groundOverlays.values.toSet(), widget.groundOverlays))); _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); } - Future _updatePolygons() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updatePolygons(GoogleMapController controller) { unawaited(controller._updatePolygons( PolygonUpdates.from(_polygons.values.toSet(), widget.polygons))); _polygons = keyByPolygonId(widget.polygons); } - Future _updatePolylines() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updatePolylines(GoogleMapController controller) { unawaited(controller._updatePolylines( PolylineUpdates.from(_polylines.values.toSet(), widget.polylines))); _polylines = keyByPolylineId(widget.polylines); } - Future _updateCircles() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updateCircles(GoogleMapController controller) { unawaited(controller._updateCircles( CircleUpdates.from(_circles.values.toSet(), widget.circles))); _circles = keyByCircleId(widget.circles); } - Future _updateHeatmaps() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updateHeatmaps(GoogleMapController controller) { unawaited( controller._updateHeatmaps( HeatmapUpdates.from(_heatmaps.values.toSet(), widget.heatmaps), @@ -527,11 +506,7 @@ class _GoogleMapState extends State { _heatmaps = keyByHeatmapId(widget.heatmaps); } - Future _updateTileOverlays() async { - final GoogleMapController controller = await _controller.future; - if (!mounted) { - return; - } + void _updateTileOverlays(GoogleMapController controller) { unawaited(controller._updateTileOverlays(widget.tileOverlays)); } @@ -542,7 +517,7 @@ class _GoogleMapState extends State { this, ); _controller.complete(controller); - unawaited(_updateTileOverlays()); + _updateTileOverlays(controller); final MapCreatedCallback? onMapCreated = widget.onMapCreated; if (onMapCreated != null) { onMapCreated(controller); From 366538fb9d3ca21e96b8d2e5e66d707cf89f990d Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Sat, 10 May 2025 02:20:12 +1200 Subject: [PATCH 3/4] Test mounted guard when updating state asynchronously from a new widget --- .../test/google_map_test.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 9276a7dbda3..a2f53c8db61 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 @@ -579,4 +579,35 @@ void main() { expect(map.mapConfiguration.style, ''); }); + + testWidgets('Update state from widget only when mounted', + (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + final State googleMapState = + tester.state(find.byType(GoogleMap)); + + await tester.pumpWidget(Container()); + + // ignore:invalid_use_of_protected_member + googleMapState.didUpdateWidget( + GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + circles: {const Circle(circleId: CircleId('circle'))}, + ), + ); + + await tester.pumpAndSettle(); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.circleUpdates.length, 1); + }); } From a1dbb38a1095c5220e0643035e4baad5a2515599 Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Sat, 10 May 2025 22:15:34 +1200 Subject: [PATCH 4/4] Only update tile overlays and notifying of map creation if map is still mounted --- .../lib/src/google_map.dart | 10 ++++--- .../fake_google_maps_flutter_platform.dart | 11 +++++++- .../test/google_map_test.dart | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) 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 53cb4869e79..27c31743db8 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 @@ -517,10 +517,12 @@ class _GoogleMapState extends State { this, ); _controller.complete(controller); - _updateTileOverlays(controller); - final MapCreatedCallback? onMapCreated = widget.onMapCreated; - if (onMapCreated != null) { - onMapCreated(controller); + if (mounted) { + _updateTileOverlays(controller); + final MapCreatedCallback? onMapCreated = widget.onMapCreated; + if (onMapCreated != null) { + onMapCreated(controller); + } } } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart index 3910fb5f9f3..448fdac7b10 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart @@ -37,8 +37,17 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { final StreamController> mapEventStreamController = StreamController>.broadcast(); + // Overrides completion of the init. + Completer? initCompleter; + @override - Future init(int mapId) async {} + Future init(int mapId) { + if (initCompleter == null) { + return Future.value(); + } + + return initCompleter!.future; + } @override Future updateMapConfiguration( 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 a2f53c8db61..9d7c30160f3 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,8 @@ // 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/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -610,4 +612,28 @@ void main() { expect(map.circleUpdates.length, 1); }); + + testWidgets('Update state after map is initialized only when mounted', + (WidgetTester tester) async { + platform.initCompleter = Completer(); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + await tester.pumpWidget(Container()); + + platform.initCompleter!.complete(); + + await tester.pumpAndSettle(); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.tileOverlaySets.length, 1); + }); }