Skip to content

Commit 754405d

Browse files
[google_map_flutter] Fix map object regression due to async changes (#4171)
In #4067 I changed several implicitly unawaited futures to `await`, not noticing that doing so would change the timing on updating fields that were used to compute diffs for future updates. Since the update flow is unawaited at higher levels, this meant that multiple calls to update map objects in rapid succession would compute incorrect diffs due to using the wrong base objects. This fixes that by restoring the old flow using `unawaited`. Adds new tests that introduce synthetic awaits into the fake platform interface object and perform multiple updates in a row that should have different base object sets, which fail without the fix. In order to simplify adding that behavior to the fake, and to pay down technical debt in the unit tests, I replaced the old test fake that is based on method channel interception (which predates the federation of the plugin and is implicitly relying on the method channel implementation from a different package never changing) with a fake platform interface implementation (substantially simplifying the fake). Fixes flutter/flutter#128042
1 parent f185bff commit 754405d

13 files changed

+783
-1315
lines changed

packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
## NEXT
1+
## 2.3.1
22

3+
* Fixes a regression from 2.2.8 that could cause incorrect handling of a
4+
rapid series of map object updates.
35
* Fixes stale ignore: prefer_const_constructors.
46
* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0.
57

packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -360,41 +360,41 @@ class _GoogleMapState extends State<GoogleMap> {
360360
return;
361361
}
362362
final GoogleMapController controller = await _controller.future;
363-
await controller._updateMapConfiguration(updates);
363+
unawaited(controller._updateMapConfiguration(updates));
364364
_mapConfiguration = newConfig;
365365
}
366366

367367
Future<void> _updateMarkers() async {
368368
final GoogleMapController controller = await _controller.future;
369-
await controller._updateMarkers(
370-
MarkerUpdates.from(_markers.values.toSet(), widget.markers));
369+
unawaited(controller._updateMarkers(
370+
MarkerUpdates.from(_markers.values.toSet(), widget.markers)));
371371
_markers = keyByMarkerId(widget.markers);
372372
}
373373

374374
Future<void> _updatePolygons() async {
375375
final GoogleMapController controller = await _controller.future;
376-
await controller._updatePolygons(
377-
PolygonUpdates.from(_polygons.values.toSet(), widget.polygons));
376+
unawaited(controller._updatePolygons(
377+
PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)));
378378
_polygons = keyByPolygonId(widget.polygons);
379379
}
380380

381381
Future<void> _updatePolylines() async {
382382
final GoogleMapController controller = await _controller.future;
383-
await controller._updatePolylines(
384-
PolylineUpdates.from(_polylines.values.toSet(), widget.polylines));
383+
unawaited(controller._updatePolylines(
384+
PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)));
385385
_polylines = keyByPolylineId(widget.polylines);
386386
}
387387

388388
Future<void> _updateCircles() async {
389389
final GoogleMapController controller = await _controller.future;
390-
await controller._updateCircles(
391-
CircleUpdates.from(_circles.values.toSet(), widget.circles));
390+
unawaited(controller._updateCircles(
391+
CircleUpdates.from(_circles.values.toSet(), widget.circles)));
392392
_circles = keyByCircleId(widget.circles);
393393
}
394394

395395
Future<void> _updateTileOverlays() async {
396396
final GoogleMapController controller = await _controller.future;
397-
await controller._updateTileOverlays(widget.tileOverlays);
397+
unawaited(controller._updateTileOverlays(widget.tileOverlays));
398398
}
399399

400400
Future<void> onPlatformViewCreated(int id) async {

packages/google_maps_flutter/google_maps_flutter/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: google_maps_flutter
22
description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
33
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
5-
version: 2.3.0
5+
version: 2.3.1
66

77
environment:
88
sdk: ">=3.0.0 <4.0.0"

packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart

Lines changed: 79 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'package:flutter/services.dart';
65
import 'package:flutter/widgets.dart';
76
import 'package:flutter_test/flutter_test.dart';
87
import 'package:google_maps_flutter/google_maps_flutter.dart';
8+
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
99

10-
import 'fake_maps_controllers.dart';
10+
import 'fake_google_maps_flutter_platform.dart';
1111

1212
Widget _mapWithCircles(Set<Circle> circles) {
1313
return Directionality(
@@ -20,36 +20,24 @@ Widget _mapWithCircles(Set<Circle> circles) {
2020
}
2121

2222
void main() {
23-
TestWidgetsFlutterBinding.ensureInitialized();
24-
25-
final FakePlatformViewsController fakePlatformViewsController =
26-
FakePlatformViewsController();
27-
28-
setUpAll(() {
29-
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
30-
.defaultBinaryMessenger
31-
.setMockMethodCallHandler(
32-
SystemChannels.platform_views,
33-
fakePlatformViewsController.fakePlatformViewsMethodHandler,
34-
);
35-
});
23+
late FakeGoogleMapsFlutterPlatform platform;
3624

3725
setUp(() {
38-
fakePlatformViewsController.reset();
26+
platform = FakeGoogleMapsFlutterPlatform();
27+
GoogleMapsFlutterPlatform.instance = platform;
3928
});
4029

4130
testWidgets('Initializing a circle', (WidgetTester tester) async {
4231
const Circle c1 = Circle(circleId: CircleId('circle_1'));
4332
await tester.pumpWidget(_mapWithCircles(<Circle>{c1}));
4433

45-
final FakePlatformGoogleMap platformGoogleMap =
46-
fakePlatformViewsController.lastCreatedView!;
47-
expect(platformGoogleMap.circlesToAdd.length, 1);
34+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
35+
expect(map.circleUpdates.last.circlesToAdd.length, 1);
4836

49-
final Circle initializedCircle = platformGoogleMap.circlesToAdd.first;
37+
final Circle initializedCircle = map.circleUpdates.last.circlesToAdd.first;
5038
expect(initializedCircle, equals(c1));
51-
expect(platformGoogleMap.circleIdsToRemove.isEmpty, true);
52-
expect(platformGoogleMap.circlesToChange.isEmpty, true);
39+
expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true);
40+
expect(map.circleUpdates.last.circlesToChange.isEmpty, true);
5341
});
5442

5543
testWidgets('Adding a circle', (WidgetTester tester) async {
@@ -59,16 +47,15 @@ void main() {
5947
await tester.pumpWidget(_mapWithCircles(<Circle>{c1}));
6048
await tester.pumpWidget(_mapWithCircles(<Circle>{c1, c2}));
6149

62-
final FakePlatformGoogleMap platformGoogleMap =
63-
fakePlatformViewsController.lastCreatedView!;
64-
expect(platformGoogleMap.circlesToAdd.length, 1);
50+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
51+
expect(map.circleUpdates.last.circlesToAdd.length, 1);
6552

66-
final Circle addedCircle = platformGoogleMap.circlesToAdd.first;
53+
final Circle addedCircle = map.circleUpdates.last.circlesToAdd.first;
6754
expect(addedCircle, equals(c2));
6855

69-
expect(platformGoogleMap.circleIdsToRemove.isEmpty, true);
56+
expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true);
7057

71-
expect(platformGoogleMap.circlesToChange.isEmpty, true);
58+
expect(map.circleUpdates.last.circlesToChange.isEmpty, true);
7259
});
7360

7461
testWidgets('Removing a circle', (WidgetTester tester) async {
@@ -77,13 +64,12 @@ void main() {
7764
await tester.pumpWidget(_mapWithCircles(<Circle>{c1}));
7865
await tester.pumpWidget(_mapWithCircles(<Circle>{}));
7966

80-
final FakePlatformGoogleMap platformGoogleMap =
81-
fakePlatformViewsController.lastCreatedView!;
82-
expect(platformGoogleMap.circleIdsToRemove.length, 1);
83-
expect(platformGoogleMap.circleIdsToRemove.first, equals(c1.circleId));
67+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
68+
expect(map.circleUpdates.last.circleIdsToRemove.length, 1);
69+
expect(map.circleUpdates.last.circleIdsToRemove.first, equals(c1.circleId));
8470

85-
expect(platformGoogleMap.circlesToChange.isEmpty, true);
86-
expect(platformGoogleMap.circlesToAdd.isEmpty, true);
71+
expect(map.circleUpdates.last.circlesToChange.isEmpty, true);
72+
expect(map.circleUpdates.last.circlesToAdd.isEmpty, true);
8773
});
8874

8975
testWidgets('Updating a circle', (WidgetTester tester) async {
@@ -93,13 +79,12 @@ void main() {
9379
await tester.pumpWidget(_mapWithCircles(<Circle>{c1}));
9480
await tester.pumpWidget(_mapWithCircles(<Circle>{c2}));
9581

96-
final FakePlatformGoogleMap platformGoogleMap =
97-
fakePlatformViewsController.lastCreatedView!;
98-
expect(platformGoogleMap.circlesToChange.length, 1);
99-
expect(platformGoogleMap.circlesToChange.first, equals(c2));
82+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
83+
expect(map.circleUpdates.last.circlesToChange.length, 1);
84+
expect(map.circleUpdates.last.circlesToChange.first, equals(c2));
10085

101-
expect(platformGoogleMap.circleIdsToRemove.isEmpty, true);
102-
expect(platformGoogleMap.circlesToAdd.isEmpty, true);
86+
expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true);
87+
expect(map.circleUpdates.last.circlesToAdd.isEmpty, true);
10388
});
10489

10590
testWidgets('Updating a circle', (WidgetTester tester) async {
@@ -109,11 +94,10 @@ void main() {
10994
await tester.pumpWidget(_mapWithCircles(<Circle>{c1}));
11095
await tester.pumpWidget(_mapWithCircles(<Circle>{c2}));
11196

112-
final FakePlatformGoogleMap platformGoogleMap =
113-
fakePlatformViewsController.lastCreatedView!;
114-
expect(platformGoogleMap.circlesToChange.length, 1);
97+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
98+
expect(map.circleUpdates.last.circlesToChange.length, 1);
11599

116-
final Circle update = platformGoogleMap.circlesToChange.first;
100+
final Circle update = map.circleUpdates.last.circlesToChange.first;
117101
expect(update, equals(c2));
118102
expect(update.radius, 10);
119103
});
@@ -129,12 +113,11 @@ void main() {
129113
await tester.pumpWidget(_mapWithCircles(prev));
130114
await tester.pumpWidget(_mapWithCircles(cur));
131115

132-
final FakePlatformGoogleMap platformGoogleMap =
133-
fakePlatformViewsController.lastCreatedView!;
116+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
134117

135-
expect(platformGoogleMap.circlesToChange, cur);
136-
expect(platformGoogleMap.circleIdsToRemove.isEmpty, true);
137-
expect(platformGoogleMap.circlesToAdd.isEmpty, true);
118+
expect(map.circleUpdates.last.circlesToChange, cur);
119+
expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true);
120+
expect(map.circleUpdates.last.circlesToAdd.isEmpty, true);
138121
});
139122

140123
testWidgets('Multi Update', (WidgetTester tester) async {
@@ -150,16 +133,15 @@ void main() {
150133
await tester.pumpWidget(_mapWithCircles(prev));
151134
await tester.pumpWidget(_mapWithCircles(cur));
152135

153-
final FakePlatformGoogleMap platformGoogleMap =
154-
fakePlatformViewsController.lastCreatedView!;
136+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
155137

156-
expect(platformGoogleMap.circlesToChange.length, 1);
157-
expect(platformGoogleMap.circlesToAdd.length, 1);
158-
expect(platformGoogleMap.circleIdsToRemove.length, 1);
138+
expect(map.circleUpdates.last.circlesToChange.length, 1);
139+
expect(map.circleUpdates.last.circlesToAdd.length, 1);
140+
expect(map.circleUpdates.last.circleIdsToRemove.length, 1);
159141

160-
expect(platformGoogleMap.circlesToChange.first, equals(c2));
161-
expect(platformGoogleMap.circlesToAdd.first, equals(c1));
162-
expect(platformGoogleMap.circleIdsToRemove.first, equals(c3.circleId));
142+
expect(map.circleUpdates.last.circlesToChange.first, equals(c2));
143+
expect(map.circleUpdates.last.circlesToAdd.first, equals(c1));
144+
expect(map.circleUpdates.last.circleIdsToRemove.first, equals(c3.circleId));
163145
});
164146

165147
testWidgets('Partial Update', (WidgetTester tester) async {
@@ -173,12 +155,11 @@ void main() {
173155
await tester.pumpWidget(_mapWithCircles(prev));
174156
await tester.pumpWidget(_mapWithCircles(cur));
175157

176-
final FakePlatformGoogleMap platformGoogleMap =
177-
fakePlatformViewsController.lastCreatedView!;
158+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
178159

179-
expect(platformGoogleMap.circlesToChange, <Circle>{c3});
180-
expect(platformGoogleMap.circleIdsToRemove.isEmpty, true);
181-
expect(platformGoogleMap.circlesToAdd.isEmpty, true);
160+
expect(map.circleUpdates.last.circlesToChange, <Circle>{c3});
161+
expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true);
162+
expect(map.circleUpdates.last.circlesToAdd.isEmpty, true);
182163
});
183164

184165
testWidgets('Update non platform related attr', (WidgetTester tester) async {
@@ -190,17 +171,42 @@ void main() {
190171
await tester.pumpWidget(_mapWithCircles(prev));
191172
await tester.pumpWidget(_mapWithCircles(cur));
192173

193-
final FakePlatformGoogleMap platformGoogleMap =
194-
fakePlatformViewsController.lastCreatedView!;
174+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
195175

196-
expect(platformGoogleMap.circlesToChange.isEmpty, true);
197-
expect(platformGoogleMap.circleIdsToRemove.isEmpty, true);
198-
expect(platformGoogleMap.circlesToAdd.isEmpty, true);
176+
expect(map.circleUpdates.last.circlesToChange.isEmpty, true);
177+
expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true);
178+
expect(map.circleUpdates.last.circlesToAdd.isEmpty, true);
199179
});
200-
}
201180

202-
/// This allows a value of type T or T? to be treated as a value of type T?.
203-
///
204-
/// We use this so that APIs that have become non-nullable can still be used
205-
/// with `!` and `?` on the stable branch.
206-
T? _ambiguate<T>(T? value) => value;
181+
testWidgets('multi-update with delays', (WidgetTester tester) async {
182+
platform.simulatePlatformDelay = true;
183+
184+
const Circle c1 = Circle(circleId: CircleId('circle_1'));
185+
const Circle c2 = Circle(circleId: CircleId('circle_2'));
186+
const Circle c3 = Circle(circleId: CircleId('circle_3'), radius: 1);
187+
const Circle c3updated = Circle(circleId: CircleId('circle_3'), radius: 10);
188+
189+
// First remove one and add another, then update the new one.
190+
await tester.pumpWidget(_mapWithCircles(<Circle>{c1, c2}));
191+
await tester.pumpWidget(_mapWithCircles(<Circle>{c1, c3}));
192+
await tester.pumpWidget(_mapWithCircles(<Circle>{c1, c3updated}));
193+
194+
final PlatformMapStateRecorder map = platform.lastCreatedMap;
195+
196+
expect(map.circleUpdates.length, 3);
197+
198+
expect(map.circleUpdates[0].circlesToChange.isEmpty, true);
199+
expect(map.circleUpdates[0].circlesToAdd, <Circle>{c1, c2});
200+
expect(map.circleUpdates[0].circleIdsToRemove.isEmpty, true);
201+
202+
expect(map.circleUpdates[1].circlesToChange.isEmpty, true);
203+
expect(map.circleUpdates[1].circlesToAdd, <Circle>{c3});
204+
expect(map.circleUpdates[1].circleIdsToRemove, <CircleId>{c2.circleId});
205+
206+
expect(map.circleUpdates[2].circlesToChange, <Circle>{c3updated});
207+
expect(map.circleUpdates[2].circlesToAdd.isEmpty, true);
208+
expect(map.circleUpdates[2].circleIdsToRemove.isEmpty, true);
209+
210+
await tester.pumpAndSettle();
211+
});
212+
}

0 commit comments

Comments
 (0)