Skip to content

[google_maps_flutter] Fixes exception when dispose is called while asynchronous update from didUpdateWidget is executed #9227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,72 +429,75 @@ class _GoogleMapState extends State<GoogleMap> {
@override
void didUpdateWidget(GoogleMap oldWidget) {
super.didUpdateWidget(oldWidget);
_updateOptions();
_updateClusterManagers();
_updateMarkers();
_updatePolygons();
_updatePolylines();
_updateCircles();
_updateHeatmaps();
_updateTileOverlays();
_updateGroundOverlays();

_refreshStateFromWidget();
}

Future<void> _updateOptions() async {
Future<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _updateHeatmaps() async {
final GoogleMapController controller = await _controller.future;
void _updateHeatmaps(GoogleMapController controller) {
unawaited(
controller._updateHeatmaps(
HeatmapUpdates.from(_heatmaps.values.toSet(), widget.heatmaps),
Expand All @@ -503,8 +506,7 @@ class _GoogleMapState extends State<GoogleMap> {
_heatmaps = keyByHeatmapId(widget.heatmaps);
}

Future<void> _updateTileOverlays() async {
final GoogleMapController controller = await _controller.future;
void _updateTileOverlays(GoogleMapController controller) {
unawaited(controller._updateTileOverlays(widget.tileOverlays));
}

Expand All @@ -515,10 +517,12 @@ class _GoogleMapState extends State<GoogleMap> {
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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,17 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {
final StreamController<MapEvent<dynamic>> mapEventStreamController =
StreamController<MapEvent<dynamic>>.broadcast();

// Overrides completion of the init.
Completer<void>? initCompleter;

@override
Future<void> init(int mapId) async {}
Future<void> init(int mapId) {
if (initCompleter == null) {
return Future<void>.value();
}

return initCompleter!.future;
}

@override
Future<void> updateMapConfiguration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<StatefulWidget> 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: <Circle>{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<void>();

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);
});
}