Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b613549
fix: Clean up subscriptions when disposing, instead of firing and for…
Apr 1, 2025
8ef7e22
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 1, 2025
3c0bcd2
refactor: format test file
Apr 1, 2025
01e0916
Merge branch 'google-maps-memory-leak' of github.com:gentlemanxzh/pac…
Apr 1, 2025
7c6c867
docs: update license
Apr 1, 2025
c620503
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 2, 2025
191937e
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 3, 2025
703cdf6
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 5, 2025
e3655a2
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 9, 2025
14e2188
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 10, 2025
702790b
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 11, 2025
b4de305
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 15, 2025
7a1b099
[google_maps_flutter]docs: update CHANGELOG and code comments
Apr 15, 2025
b7f3a47
[google_maps_flutter]test: adjust the unit test
Apr 15, 2025
98fb6e9
[google_maps_flutter]format: format unit test code
Apr 15, 2025
5a37f83
[google_maps_flutter]format: format controller code
Apr 15, 2025
ce87e7d
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 16, 2025
5cf5f95
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 17, 2025
3f063de
refactor: optimize the test file
Apr 23, 2025
651c98a
docs: update CHANGELOG file
Apr 23, 2025
b56b95b
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 23, 2025
8e9ab88
refactor: format test file
Apr 23, 2025
5d81596
Merge branch 'google-maps-memory-leak' of github.com:gentlemanxzh/pac…
Apr 23, 2025
fee9f73
docs: update version
Apr 23, 2025
84418a8
docs: update CHANGELOG and version
Apr 23, 2025
b8a32fb
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 24, 2025
d72408e
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh Apr 27, 2025
0564b68
Merge branch 'main' into google-maps-memory-leak
gentlemanxzh May 8, 2025
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
1 change: 1 addition & 0 deletions packages/google_maps_flutter/google_maps_flutter/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ Alex Li <google@alexv525.com>
Rahul Raj <64.rahulraj@gmail.com>
Taha Tesser <tesser@gmail.com>
Joonas Kerttula <joonas.kerttula@codemate.com>
gentlemanxzh <gentlemanxzh@gmail.com>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 2.12.2

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

## 2.12.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ class GoogleMapController {
/// The mapId for this controller
final int mapId;

/// List of active stream subscriptions for map events.
///
/// This list keeps track of all event subscriptions created for the map,
/// including camera movements, marker interactions, and other map events.
/// These subscriptions should be disposed when the controller is disposed.
final List<StreamSubscription<dynamic>> _streamSubscriptions =
<StreamSubscription<dynamic>>[];

/// Initialize control of a [GoogleMap] with [id].
///
/// Mainly for internal use when instantiating a [GoogleMapController] passed
Expand All @@ -38,53 +46,85 @@ class GoogleMapController {

void _connectStreams(int mapId) {
if (_googleMapState.widget.onCameraMoveStarted != null) {
GoogleMapsFlutterPlatform.instance
.onCameraMoveStarted(mapId: mapId)
.listen((_) => _googleMapState.widget.onCameraMoveStarted!());
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance
.onCameraMoveStarted(mapId: mapId)
.listen((_) => _googleMapState.widget.onCameraMoveStarted!()),
);
}
if (_googleMapState.widget.onCameraMove != null) {
GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen(
(CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value));
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen(
(CameraMoveEvent e) =>
_googleMapState.widget.onCameraMove!(e.value),
),
);
}
if (_googleMapState.widget.onCameraIdle != null) {
GoogleMapsFlutterPlatform.instance
.onCameraIdle(mapId: mapId)
.listen((_) => _googleMapState.widget.onCameraIdle!());
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance
.onCameraIdle(mapId: mapId)
.listen((_) => _googleMapState.widget.onCameraIdle!()),
);
}
GoogleMapsFlutterPlatform.instance
.onMarkerTap(mapId: mapId)
.listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value));
GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen(
(MarkerDragStartEvent e) =>
_googleMapState.onMarkerDragStart(e.value, e.position));
GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen(
(MarkerDragEvent e) =>
_googleMapState.onMarkerDrag(e.value, e.position));
GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen(
(MarkerDragEndEvent e) =>
_googleMapState.onMarkerDragEnd(e.value, e.position));
GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen(
(InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value));
GoogleMapsFlutterPlatform.instance
.onPolylineTap(mapId: mapId)
.listen((PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value));
GoogleMapsFlutterPlatform.instance
.onPolygonTap(mapId: mapId)
.listen((PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value));
GoogleMapsFlutterPlatform.instance
.onCircleTap(mapId: mapId)
.listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value));
GoogleMapsFlutterPlatform.instance.onGroundOverlayTap(mapId: mapId).listen(
(GroundOverlayTapEvent e) =>
_googleMapState.onGroundOverlayTap(e.value));
GoogleMapsFlutterPlatform.instance
.onTap(mapId: mapId)
.listen((MapTapEvent e) => _googleMapState.onTap(e.position));
GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen(
(MapLongPressEvent e) => _googleMapState.onLongPress(e.position));
GoogleMapsFlutterPlatform.instance
.onClusterTap(mapId: mapId)
.listen((ClusterTapEvent e) => _googleMapState.onClusterTap(e.value));
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance
.onMarkerTap(mapId: mapId)
.listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen(
(MarkerDragStartEvent e) =>
_googleMapState.onMarkerDragStart(e.value, e.position),
),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen(
(MarkerDragEvent e) =>
_googleMapState.onMarkerDrag(e.value, e.position),
),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen(
(MarkerDragEndEvent e) =>
_googleMapState.onMarkerDragEnd(e.value, e.position),
),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen(
(InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value),
),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onPolylineTap(mapId: mapId).listen(
(PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value),
),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onPolygonTap(mapId: mapId).listen(
(PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value),
),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance
.onCircleTap(mapId: mapId)
.listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance
.onTap(mapId: mapId)
.listen((MapTapEvent e) => _googleMapState.onTap(e.position)),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen(
(MapLongPressEvent e) => _googleMapState.onLongPress(e.position),
),
);
_streamSubscriptions.add(
GoogleMapsFlutterPlatform.instance.onClusterTap(mapId: mapId).listen(
(ClusterTapEvent e) => _googleMapState.onClusterTap(e.value),
),
);
}

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

/// Disposes of the platform resources
void dispose() {
for (final StreamSubscription<dynamic> streamSubscription
in _streamSubscriptions) {
streamSubscription.cancel();
}
_streamSubscriptions.clear();
GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId);
}
}
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.1
version: 2.12.2

environment:
sdk: ^3.6.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// 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';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';

import 'fake_google_maps_flutter_platform.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

testWidgets('Subscriptions are canceled on dispose',
(WidgetTester tester) async {
final FakeGoogleMapsFlutterPlatform platform =
FakeGoogleMapsFlutterPlatform();

GoogleMapsFlutterPlatform.instance = platform;

final Completer<GoogleMapController?> controllerCompleter =
Completer<GoogleMapController?>();

final GoogleMap googleMap = GoogleMap(
onMapCreated: (GoogleMapController controller) {
controllerCompleter.complete(controller);
},
initialCameraPosition: const CameraPosition(
target: LatLng(0, 0),
),
);

await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: googleMap,
));

await tester.pump();

final GoogleMapController? controller = await controllerCompleter.future;

if (controller == null) {
fail('GoogleMapController not created');
}

expect(platform.mapEventStreamController.hasListener, true);

// Remove the map from the widget tree.
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(),
));

await tester.binding.runAsync(() async {
await tester.pump();
});

expect(platform.mapEventStreamController.hasListener, false);
});
}