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..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 @@ -429,72 +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 _updateOptions() async { + 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); + } + + 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; + unawaited(controller._updateMapConfiguration(updates)); _mapConfiguration = newConfig; } - Future _updateMarkers() async { - final GoogleMapController controller = await _controller.future; + 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; + 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; + 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; + 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; + 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; + 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; + void _updateHeatmaps(GoogleMapController controller) { unawaited( controller._updateHeatmaps( HeatmapUpdates.from(_heatmaps.values.toSet(), widget.heatmaps), @@ -503,8 +506,7 @@ class _GoogleMapState extends State { _heatmaps = keyByHeatmapId(widget.heatmaps); } - Future _updateTileOverlays() async { - final GoogleMapController controller = await _controller.future; + void _updateTileOverlays(GoogleMapController controller) { unawaited(controller._updateTileOverlays(widget.tileOverlays)); } @@ -515,10 +517,12 @@ class _GoogleMapState extends State { this, ); _controller.complete(controller); - unawaited(_updateTileOverlays()); - 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/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 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 9276a7dbda3..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'; @@ -579,4 +581,59 @@ 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); + }); + + 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); + }); }