Skip to content

Commit ab44c26

Browse files
authored
[google_maps_flutter] Fix for memory leak impacting all platforms due to subscriptions not getting cleaned up (#8972)
This PR is #4281 follow-up,Fix map memory leak *List which issues are fixed by this PR. You must list at least one issue.* [#129089](flutter/flutter#129089) Fixes [#115283](flutter/flutter#115283) [#92788](flutter/flutter#92788) ## Pre-Review Checklist
1 parent 1392e8c commit ab44c26

File tree

5 files changed

+154
-44
lines changed

5 files changed

+154
-44
lines changed

packages/google_maps_flutter/google_maps_flutter/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,4 @@ Alex Li <google@alexv525.com>
6666
Rahul Raj <64.rahulraj@gmail.com>
6767
Taha Tesser <tesser@gmail.com>
6868
Joonas Kerttula <joonas.kerttula@codemate.com>
69+
gentlemanxzh <gentlemanxzh@gmail.com>

packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 2.12.2
22

3+
* Fixes memory leak by disposing stream subscriptions in `GoogleMapController`.
34
* Updates README to indicate that Andoid SDK <21 is no longer supported.
45

56
## 2.12.1

packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ class GoogleMapController {
1818
/// The mapId for this controller
1919
final int mapId;
2020

21+
/// List of active stream subscriptions for map events.
22+
///
23+
/// This list keeps track of all event subscriptions created for the map,
24+
/// including camera movements, marker interactions, and other map events.
25+
/// These subscriptions should be disposed when the controller is disposed.
26+
final List<StreamSubscription<dynamic>> _streamSubscriptions =
27+
<StreamSubscription<dynamic>>[];
28+
2129
/// Initialize control of a [GoogleMap] with [id].
2230
///
2331
/// Mainly for internal use when instantiating a [GoogleMapController] passed
@@ -38,53 +46,85 @@ class GoogleMapController {
3846

3947
void _connectStreams(int mapId) {
4048
if (_googleMapState.widget.onCameraMoveStarted != null) {
41-
GoogleMapsFlutterPlatform.instance
42-
.onCameraMoveStarted(mapId: mapId)
43-
.listen((_) => _googleMapState.widget.onCameraMoveStarted!());
49+
_streamSubscriptions.add(
50+
GoogleMapsFlutterPlatform.instance
51+
.onCameraMoveStarted(mapId: mapId)
52+
.listen((_) => _googleMapState.widget.onCameraMoveStarted!()),
53+
);
4454
}
4555
if (_googleMapState.widget.onCameraMove != null) {
46-
GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen(
47-
(CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value));
56+
_streamSubscriptions.add(
57+
GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen(
58+
(CameraMoveEvent e) =>
59+
_googleMapState.widget.onCameraMove!(e.value),
60+
),
61+
);
4862
}
4963
if (_googleMapState.widget.onCameraIdle != null) {
50-
GoogleMapsFlutterPlatform.instance
51-
.onCameraIdle(mapId: mapId)
52-
.listen((_) => _googleMapState.widget.onCameraIdle!());
64+
_streamSubscriptions.add(
65+
GoogleMapsFlutterPlatform.instance
66+
.onCameraIdle(mapId: mapId)
67+
.listen((_) => _googleMapState.widget.onCameraIdle!()),
68+
);
5369
}
54-
GoogleMapsFlutterPlatform.instance
55-
.onMarkerTap(mapId: mapId)
56-
.listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value));
57-
GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen(
58-
(MarkerDragStartEvent e) =>
59-
_googleMapState.onMarkerDragStart(e.value, e.position));
60-
GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen(
61-
(MarkerDragEvent e) =>
62-
_googleMapState.onMarkerDrag(e.value, e.position));
63-
GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen(
64-
(MarkerDragEndEvent e) =>
65-
_googleMapState.onMarkerDragEnd(e.value, e.position));
66-
GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen(
67-
(InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value));
68-
GoogleMapsFlutterPlatform.instance
69-
.onPolylineTap(mapId: mapId)
70-
.listen((PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value));
71-
GoogleMapsFlutterPlatform.instance
72-
.onPolygonTap(mapId: mapId)
73-
.listen((PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value));
74-
GoogleMapsFlutterPlatform.instance
75-
.onCircleTap(mapId: mapId)
76-
.listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value));
77-
GoogleMapsFlutterPlatform.instance.onGroundOverlayTap(mapId: mapId).listen(
78-
(GroundOverlayTapEvent e) =>
79-
_googleMapState.onGroundOverlayTap(e.value));
80-
GoogleMapsFlutterPlatform.instance
81-
.onTap(mapId: mapId)
82-
.listen((MapTapEvent e) => _googleMapState.onTap(e.position));
83-
GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen(
84-
(MapLongPressEvent e) => _googleMapState.onLongPress(e.position));
85-
GoogleMapsFlutterPlatform.instance
86-
.onClusterTap(mapId: mapId)
87-
.listen((ClusterTapEvent e) => _googleMapState.onClusterTap(e.value));
70+
_streamSubscriptions.add(
71+
GoogleMapsFlutterPlatform.instance
72+
.onMarkerTap(mapId: mapId)
73+
.listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)),
74+
);
75+
_streamSubscriptions.add(
76+
GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen(
77+
(MarkerDragStartEvent e) =>
78+
_googleMapState.onMarkerDragStart(e.value, e.position),
79+
),
80+
);
81+
_streamSubscriptions.add(
82+
GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen(
83+
(MarkerDragEvent e) =>
84+
_googleMapState.onMarkerDrag(e.value, e.position),
85+
),
86+
);
87+
_streamSubscriptions.add(
88+
GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen(
89+
(MarkerDragEndEvent e) =>
90+
_googleMapState.onMarkerDragEnd(e.value, e.position),
91+
),
92+
);
93+
_streamSubscriptions.add(
94+
GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen(
95+
(InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value),
96+
),
97+
);
98+
_streamSubscriptions.add(
99+
GoogleMapsFlutterPlatform.instance.onPolylineTap(mapId: mapId).listen(
100+
(PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value),
101+
),
102+
);
103+
_streamSubscriptions.add(
104+
GoogleMapsFlutterPlatform.instance.onPolygonTap(mapId: mapId).listen(
105+
(PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value),
106+
),
107+
);
108+
_streamSubscriptions.add(
109+
GoogleMapsFlutterPlatform.instance
110+
.onCircleTap(mapId: mapId)
111+
.listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)),
112+
);
113+
_streamSubscriptions.add(
114+
GoogleMapsFlutterPlatform.instance
115+
.onTap(mapId: mapId)
116+
.listen((MapTapEvent e) => _googleMapState.onTap(e.position)),
117+
);
118+
_streamSubscriptions.add(
119+
GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen(
120+
(MapLongPressEvent e) => _googleMapState.onLongPress(e.position),
121+
),
122+
);
123+
_streamSubscriptions.add(
124+
GoogleMapsFlutterPlatform.instance.onClusterTap(mapId: mapId).listen(
125+
(ClusterTapEvent e) => _googleMapState.onClusterTap(e.value),
126+
),
127+
);
88128
}
89129

90130
/// Updates configuration options of the map user interface.
@@ -321,6 +361,11 @@ class GoogleMapController {
321361

322362
/// Disposes of the platform resources
323363
void dispose() {
364+
for (final StreamSubscription<dynamic> streamSubscription
365+
in _streamSubscriptions) {
366+
streamSubscription.cancel();
367+
}
368+
_streamSubscriptions.clear();
324369
GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId);
325370
}
326371
}

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.12.1
5+
version: 2.12.2
66

77
environment:
88
sdk: ^3.6.0
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:flutter/widgets.dart';
8+
import 'package:flutter_test/flutter_test.dart';
9+
import 'package:google_maps_flutter/google_maps_flutter.dart';
10+
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
11+
12+
import 'fake_google_maps_flutter_platform.dart';
13+
14+
void main() {
15+
TestWidgetsFlutterBinding.ensureInitialized();
16+
17+
testWidgets('Subscriptions are canceled on dispose',
18+
(WidgetTester tester) async {
19+
final FakeGoogleMapsFlutterPlatform platform =
20+
FakeGoogleMapsFlutterPlatform();
21+
22+
GoogleMapsFlutterPlatform.instance = platform;
23+
24+
final Completer<GoogleMapController?> controllerCompleter =
25+
Completer<GoogleMapController?>();
26+
27+
final GoogleMap googleMap = GoogleMap(
28+
onMapCreated: (GoogleMapController controller) {
29+
controllerCompleter.complete(controller);
30+
},
31+
initialCameraPosition: const CameraPosition(
32+
target: LatLng(0, 0),
33+
),
34+
);
35+
36+
await tester.pumpWidget(Directionality(
37+
textDirection: TextDirection.ltr,
38+
child: googleMap,
39+
));
40+
41+
await tester.pump();
42+
43+
final GoogleMapController? controller = await controllerCompleter.future;
44+
45+
if (controller == null) {
46+
fail('GoogleMapController not created');
47+
}
48+
49+
expect(platform.mapEventStreamController.hasListener, true);
50+
51+
// Remove the map from the widget tree.
52+
await tester.pumpWidget(const Directionality(
53+
textDirection: TextDirection.ltr,
54+
child: SizedBox(),
55+
));
56+
57+
await tester.binding.runAsync(() async {
58+
await tester.pump();
59+
});
60+
61+
expect(platform.mapEventStreamController.hasListener, false);
62+
});
63+
}

0 commit comments

Comments
 (0)