diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index e9c519906b8..c165987e87b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.13.1 + +* Fixes exception when dispose is called while asynchronous update from + `didUpdateWidget` is executed. + ## 2.13.0 * Adds support for camera control button on web. 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 98cfd747d41..ab529764cc1 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 @@ -448,30 +448,38 @@ 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; 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), @@ -480,8 +488,7 @@ class _GoogleMapState extends State { _markers = keyByMarkerId(widget.markers); } - Future _updateClusterManagers() async { - final GoogleMapController controller = await _controller.future; + void _updateClusterManagers(GoogleMapController controller) { unawaited( controller._updateClusterManagers( ClusterManagerUpdates.from( @@ -493,8 +500,7 @@ class _GoogleMapState extends State { _clusterManagers = keyByClusterManagerId(widget.clusterManagers); } - Future _updateGroundOverlays() async { - final GoogleMapController controller = await _controller.future; + void _updateGroundOverlays(GoogleMapController controller) { unawaited( controller._updateGroundOverlays( GroundOverlayUpdates.from( @@ -506,8 +512,7 @@ class _GoogleMapState extends State { _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), @@ -516,8 +521,7 @@ class _GoogleMapState extends State { _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), @@ -526,8 +530,7 @@ class _GoogleMapState extends State { _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), @@ -536,8 +539,7 @@ class _GoogleMapState extends State { _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), @@ -546,8 +548,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)); } @@ -558,10 +559,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 b5f487c8c7f..dfa32974c48 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.13.0 +version: 2.13.1 environment: sdk: ^3.7.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 093ef96b6d5..41f60601d86 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,11 @@ 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) async => 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 5b17360128f..038ca8f5ace 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'; @@ -598,4 +600,63 @@ 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()); + + // This is done to force the update path while the widget is not mounted. + // 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); + }); }