From b6135490d03d8f5f6d7fc69f0f17dc97b483754e Mon Sep 17 00:00:00 2001 From: 10909 Date: Tue, 1 Apr 2025 16:44:15 +0800 Subject: [PATCH 01/12] fix: Clean up subscriptions when disposing, instead of firing and forgetting --- .../google_maps_flutter/AUTHORS | 1 + .../google_maps_flutter/CHANGELOG.md | 6 + .../lib/src/controller.dart | 152 +++++++++++++----- .../google_maps_flutter/pubspec.yaml | 2 +- .../test/controller_test.dart | 127 +++++++++++++++ 5 files changed, 245 insertions(+), 43 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart diff --git a/packages/google_maps_flutter/google_maps_flutter/AUTHORS b/packages/google_maps_flutter/google_maps_flutter/AUTHORS index 4fc3ace39f0..2f113a9ea83 100644 --- a/packages/google_maps_flutter/google_maps_flutter/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter/AUTHORS @@ -66,3 +66,4 @@ Alex Li Rahul Raj <64.rahulraj@gmail.com> Taha Tesser Joonas Kerttula +gentlemanxzh diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index fba99c45dbf..88b8012454c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.12.2 + +* Fixes memory leak by implementing mechanism for disposing stream subscriptions in `GoogleMapController`. + Previously, subscriptions were not canceled upon disposal of the controller, which could lead to memory leaks or + unexpected behavior. The controller now tracks all stream subscriptions and cancels them when `dispose()` is called. + ## 2.12.1 * Fixes typo in README. diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 46fc95a70d2..e5ac843829f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -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 are properly disposed when the controller is disposed. + final List> _streamSubscriptionList = + >[]; + /// Initialize control of a [GoogleMap] with [id]. /// /// Mainly for internal use when instantiating a [GoogleMapController] passed @@ -38,53 +46,108 @@ class GoogleMapController { void _connectStreams(int mapId) { if (_googleMapState.widget.onCameraMoveStarted != null) { - GoogleMapsFlutterPlatform.instance - .onCameraMoveStarted(mapId: mapId) - .listen((_) => _googleMapState.widget.onCameraMoveStarted!()); + _addSubscription( + 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)); + _addSubscription( + 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!()); + _addSubscription( + 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)); + _addSubscription( + GoogleMapsFlutterPlatform.instance + .onMarkerTap(mapId: mapId) + .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen( + (MarkerDragStartEvent e) => + _googleMapState.onMarkerDragStart(e.value, e.position), + ), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen( + (MarkerDragEvent e) => + _googleMapState.onMarkerDrag(e.value, e.position), + ), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( + (MarkerDragEndEvent e) => + _googleMapState.onMarkerDragEnd(e.value, e.position), + ), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen( + (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value), + ), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onPolylineTap(mapId: mapId).listen( + (PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value), + ), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onPolygonTap(mapId: mapId).listen( + (PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value), + ), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance + .onCircleTap(mapId: mapId) + .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance + .onTap(mapId: mapId) + .listen((MapTapEvent e) => _googleMapState.onTap(e.position)), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( + (MapLongPressEvent e) => _googleMapState.onLongPress(e.position), + ), + ); + _addSubscription( + GoogleMapsFlutterPlatform.instance.onClusterTap(mapId: mapId).listen( + (ClusterTapEvent e) => _googleMapState.onClusterTap(e.value), + ), + ); + } + + /// Adds a stream subscription to the list of active subscriptions. + /// + /// This method is used to track and manage all stream subscriptions + /// created for map events. The subscriptions are stored in + /// [_streamSubscriptionList] and will be properly disposed when the + /// controller is disposed. + void _addSubscription(StreamSubscription subscription) { + _streamSubscriptionList.add(subscription); + } + + /// Adds a stream subscription to the tracked list for testing purposes. + /// + /// This method allows tests to add custom stream subscriptions to the + /// controller's tracking list. It is only intended for use in tests and + /// should not be called in production code. + /// + /// The added subscription will be properly canceled when the controller + /// is disposed, allowing tests to verify subscription cleanup behavior. + @visibleForTesting + void addDebugTrackSubscription(StreamSubscription subscription) { + _addSubscription(subscription); } /// Updates configuration options of the map user interface. @@ -321,6 +384,11 @@ class GoogleMapController { /// Disposes of the platform resources void dispose() { + for (final StreamSubscription streamSubscription + in _streamSubscriptionList) { + streamSubscription.cancel(); + } + _streamSubscriptionList.clear(); GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId); } } diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 9a23a1d5253..974bbaa2101 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.12.1 +version: 2.12.2 environment: sdk: ^3.6.0 diff --git a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart new file mode 100644 index 00000000000..f8fa0086382 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart @@ -0,0 +1,127 @@ +// 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/services.dart'; +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 { + GoogleMapsFlutterPlatform.instance = FakeGoogleMapsFlutterPlatform(); + + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); + + final ValueNotifier controllerNotifier = + ValueNotifier(null); + + final GoogleMap googleMap = GoogleMap( + onMapCreated: (GoogleMapController controller) { + controllerNotifier.value = controller; + }, + initialCameraPosition: const CameraPosition( + target: LatLng(0, 0), + ), + ); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: googleMap, + )); + + await tester.pump(); + + final GoogleMapController? controller = controllerNotifier.value; + + if (controller != null) { + final TrackableStreamSubscription + subscription = TrackableStreamSubscription(); + + controller.addDebugTrackSubscription(subscription); + expect(subscription.isCanceled, false); + + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.ltr, + child: SizedBox(), + )); + + await tester.binding.runAsync(() async { + await tester.pump(); + }); + + expect(subscription.isCanceled, true); + + controllerNotifier.dispose(); + } else { + fail('GoogleMapController not created'); + } + }); +} + +class FakePlatformViewsController { + Future fakePlatformViewsMethodHandler(MethodCall call) { + switch (call.method) { + case 'create': + return Future.value(1); + default: + return Future.value(); + } + } +} + +/// A trackable implementation of [StreamSubscription] that records cancellation. +/// +/// This class is used for testing purposes to verify that stream subscriptions +/// are properly cancelled when a [GoogleMapController] is disposed. +/// +/// It implements the minimum functionality needed to act as a +/// [StreamSubscription] while tracking whether [cancel] has been called. +class TrackableStreamSubscription implements StreamSubscription { + bool _canceled = false; + + bool get isCanceled => _canceled; + + @override + Future cancel() async { + _canceled = true; + return Future.value(); + } + + @override + bool get isPaused => false; + + @override + void onData(void Function(T data)? handleData) {} + + @override + void onDone(void Function()? handleDone) {} + + @override + void onError(Function? handleError) {} + + @override + void pause([Future? resumeSignal]) {} + + @override + void resume() {} + + @override + Future asFuture([E? futureValue]) { + return Future.value(futureValue as E); + } +} From 3c0bcd2bdcad922905c7c0953dad76ec1b27cfb4 Mon Sep 17 00:00:00 2001 From: 10909 Date: Tue, 1 Apr 2025 17:31:38 +0800 Subject: [PATCH 02/12] refactor: format test file --- .../test/controller_test.dart | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart index f8fa0086382..539ae685bfc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart @@ -15,62 +15,62 @@ import 'fake_google_maps_flutter_platform.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Subscriptions are canceled on dispose', - (WidgetTester tester) async { - GoogleMapsFlutterPlatform.instance = FakeGoogleMapsFlutterPlatform(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - - final ValueNotifier controllerNotifier = - ValueNotifier(null); - - final GoogleMap googleMap = GoogleMap( - onMapCreated: (GoogleMapController controller) { - controllerNotifier.value = controller; - }, - initialCameraPosition: const CameraPosition( - target: LatLng(0, 0), - ), - ); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: googleMap, - )); + testWidgets('Subscriptions are canceled on dispose', + (WidgetTester tester) async { + GoogleMapsFlutterPlatform.instance = FakeGoogleMapsFlutterPlatform(); + + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); + + final ValueNotifier controllerNotifier = + ValueNotifier(null); - await tester.pump(); + final GoogleMap googleMap = GoogleMap( + onMapCreated: (GoogleMapController controller) { + controllerNotifier.value = controller; + }, + initialCameraPosition: const CameraPosition( + target: LatLng(0, 0), + ), + ); - final GoogleMapController? controller = controllerNotifier.value; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: googleMap, + )); - if (controller != null) { - final TrackableStreamSubscription - subscription = TrackableStreamSubscription(); + await tester.pump(); - controller.addDebugTrackSubscription(subscription); - expect(subscription.isCanceled, false); + final GoogleMapController? controller = controllerNotifier.value; - await tester.pumpWidget(const Directionality( - textDirection: TextDirection.ltr, - child: SizedBox(), - )); + if (controller != null) { + final TrackableStreamSubscription subscription = + TrackableStreamSubscription(); - await tester.binding.runAsync(() async { - await tester.pump(); - }); + controller.addDebugTrackSubscription(subscription); + expect(subscription.isCanceled, false); - expect(subscription.isCanceled, true); + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.ltr, + child: SizedBox(), + )); + + await tester.binding.runAsync(() async { + await tester.pump(); + }); - controllerNotifier.dispose(); - } else { - fail('GoogleMapController not created'); - } - }); + expect(subscription.isCanceled, true); + + controllerNotifier.dispose(); + } else { + fail('GoogleMapController not created'); + } + }); } class FakePlatformViewsController { From 7c6c8679a28a17695868cdfa7b15df4dfbc7745e Mon Sep 17 00:00:00 2001 From: 10909 Date: Tue, 1 Apr 2025 17:51:12 +0800 Subject: [PATCH 03/12] docs: update license --- .../google_maps_flutter/test/controller_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart index 539ae685bfc..20b8e3dbcee 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved +// 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. From 7a1b099dfb0d60464b2cc2eedea4248d25426a75 Mon Sep 17 00:00:00 2001 From: 10909 Date: Tue, 15 Apr 2025 15:18:37 +0800 Subject: [PATCH 04/12] [google_maps_flutter]docs: update CHANGELOG and code comments --- .../google_maps_flutter/CHANGELOG.md | 4 +- .../lib/src/controller.dart | 58 ++++++------------- 2 files changed, 19 insertions(+), 43 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 88b8012454c..69001816bf2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,8 +1,6 @@ ## 2.12.2 -* Fixes memory leak by implementing mechanism for disposing stream subscriptions in `GoogleMapController`. - Previously, subscriptions were not canceled upon disposal of the controller, which could lead to memory leaks or - unexpected behavior. The controller now tracks all stream subscriptions and cancels them when `dispose()` is called. +* Fixes memory leak by disposing stream subscriptions in `GoogleMapController`. ## 2.12.1 diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index e5ac843829f..229cb556970 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -22,8 +22,8 @@ class GoogleMapController { /// /// This list keeps track of all event subscriptions created for the map, /// including camera movements, marker interactions, and other map events. - /// These subscriptions are properly disposed when the controller is disposed. - final List> _streamSubscriptionList = + /// These subscriptions should be disposed when the controller is disposed. + final List> _streamSubscriptions = >[]; /// Initialize control of a [GoogleMap] with [id]. @@ -46,14 +46,14 @@ class GoogleMapController { void _connectStreams(int mapId) { if (_googleMapState.widget.onCameraMoveStarted != null) { - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance .onCameraMoveStarted(mapId: mapId) .listen((_) => _googleMapState.widget.onCameraMoveStarted!()), ); } if (_googleMapState.widget.onCameraMove != null) { - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen( (CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value), @@ -61,94 +61,72 @@ class GoogleMapController { ); } if (_googleMapState.widget.onCameraIdle != null) { - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance .onCameraIdle(mapId: mapId) .listen((_) => _googleMapState.widget.onCameraIdle!()), ); } - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance .onMarkerTap(mapId: mapId) .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen( (MarkerDragStartEvent e) => _googleMapState.onMarkerDragStart(e.value, e.position), ), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen( (MarkerDragEvent e) => _googleMapState.onMarkerDrag(e.value, e.position), ), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( (MarkerDragEndEvent e) => _googleMapState.onMarkerDragEnd(e.value, e.position), ), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen( (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value), ), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onPolylineTap(mapId: mapId).listen( (PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value), ), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onPolygonTap(mapId: mapId).listen( (PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value), ), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance .onCircleTap(mapId: mapId) .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance .onTap(mapId: mapId) .listen((MapTapEvent e) => _googleMapState.onTap(e.position)), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( (MapLongPressEvent e) => _googleMapState.onLongPress(e.position), ), ); - _addSubscription( + _streamSubscriptions.add( GoogleMapsFlutterPlatform.instance.onClusterTap(mapId: mapId).listen( (ClusterTapEvent e) => _googleMapState.onClusterTap(e.value), ), ); } - /// Adds a stream subscription to the list of active subscriptions. - /// - /// This method is used to track and manage all stream subscriptions - /// created for map events. The subscriptions are stored in - /// [_streamSubscriptionList] and will be properly disposed when the - /// controller is disposed. - void _addSubscription(StreamSubscription subscription) { - _streamSubscriptionList.add(subscription); - } - - /// Adds a stream subscription to the tracked list for testing purposes. - /// - /// This method allows tests to add custom stream subscriptions to the - /// controller's tracking list. It is only intended for use in tests and - /// should not be called in production code. - /// - /// The added subscription will be properly canceled when the controller - /// is disposed, allowing tests to verify subscription cleanup behavior. - @visibleForTesting - void addDebugTrackSubscription(StreamSubscription subscription) { - _addSubscription(subscription); - } /// Updates configuration options of the map user interface. /// @@ -385,10 +363,10 @@ class GoogleMapController { /// Disposes of the platform resources void dispose() { for (final StreamSubscription streamSubscription - in _streamSubscriptionList) { + in _streamSubscriptions) { streamSubscription.cancel(); } - _streamSubscriptionList.clear(); + _streamSubscriptions.clear(); GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId); } } From b7f3a4719b88230caffe5d1c65d347563b01eef8 Mon Sep 17 00:00:00 2001 From: 10909 Date: Tue, 15 Apr 2025 15:19:33 +0800 Subject: [PATCH 05/12] [google_maps_flutter]test: adjust the unit test --- .../test/controller_test.dart | 56 ++----------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart index 20b8e3dbcee..35e7265ead9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart @@ -2,7 +2,6 @@ // 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/services.dart'; import 'package:flutter/widgets.dart'; @@ -17,7 +16,10 @@ void main() { testWidgets('Subscriptions are canceled on dispose', (WidgetTester tester) async { - GoogleMapsFlutterPlatform.instance = FakeGoogleMapsFlutterPlatform(); + final FakeGoogleMapsFlutterPlatform platform = + FakeGoogleMapsFlutterPlatform(); + + GoogleMapsFlutterPlatform.instance = platform; final FakePlatformViewsController fakePlatformViewsController = FakePlatformViewsController(); @@ -49,11 +51,7 @@ void main() { final GoogleMapController? controller = controllerNotifier.value; if (controller != null) { - final TrackableStreamSubscription subscription = - TrackableStreamSubscription(); - - controller.addDebugTrackSubscription(subscription); - expect(subscription.isCanceled, false); + expect(platform.mapEventStreamController.hasListener, true); await tester.pumpWidget(const Directionality( textDirection: TextDirection.ltr, @@ -64,7 +62,7 @@ void main() { await tester.pump(); }); - expect(subscription.isCanceled, true); + expect(platform.mapEventStreamController.hasListener, false); controllerNotifier.dispose(); } else { @@ -83,45 +81,3 @@ class FakePlatformViewsController { } } } - -/// A trackable implementation of [StreamSubscription] that records cancellation. -/// -/// This class is used for testing purposes to verify that stream subscriptions -/// are properly cancelled when a [GoogleMapController] is disposed. -/// -/// It implements the minimum functionality needed to act as a -/// [StreamSubscription] while tracking whether [cancel] has been called. -class TrackableStreamSubscription implements StreamSubscription { - bool _canceled = false; - - bool get isCanceled => _canceled; - - @override - Future cancel() async { - _canceled = true; - return Future.value(); - } - - @override - bool get isPaused => false; - - @override - void onData(void Function(T data)? handleData) {} - - @override - void onDone(void Function()? handleDone) {} - - @override - void onError(Function? handleError) {} - - @override - void pause([Future? resumeSignal]) {} - - @override - void resume() {} - - @override - Future asFuture([E? futureValue]) { - return Future.value(futureValue as E); - } -} From 98fb6e959c7909fb9646e2561669a3d658c86746 Mon Sep 17 00:00:00 2001 From: 10909 Date: Tue, 15 Apr 2025 15:33:51 +0800 Subject: [PATCH 06/12] [google_maps_flutter]format: format unit test code --- .../google_maps_flutter/test/controller_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart index 35e7265ead9..1d9cc9815f7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -18,7 +17,7 @@ void main() { (WidgetTester tester) async { final FakeGoogleMapsFlutterPlatform platform = FakeGoogleMapsFlutterPlatform(); - + GoogleMapsFlutterPlatform.instance = platform; final FakePlatformViewsController fakePlatformViewsController = From 5a37f8338200a00182963173ef03ef0c3e8ea053 Mon Sep 17 00:00:00 2001 From: 10909 Date: Tue, 15 Apr 2025 15:44:11 +0800 Subject: [PATCH 07/12] [google_maps_flutter]format: format controller code --- .../google_maps_flutter/lib/src/controller.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 229cb556970..50b0641f68a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -127,7 +127,6 @@ class GoogleMapController { ); } - /// Updates configuration options of the map user interface. /// /// Change listeners are notified once the update has been made on the From 3f063def0033ba9c77365f20d6527e82abf02887 Mon Sep 17 00:00:00 2001 From: 10909 Date: Wed, 23 Apr 2025 09:46:50 +0800 Subject: [PATCH 08/12] refactor: optimize the test file --- .../test/controller_test.dart | 45 ++++++++----------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart index 1d9cc9815f7..7aa3826650e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/controller_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/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,20 +22,12 @@ void main() { GoogleMapsFlutterPlatform.instance = platform; - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - - final ValueNotifier controllerNotifier = - ValueNotifier(null); + final Completer controllerCompleter = + Completer(); final GoogleMap googleMap = GoogleMap( onMapCreated: (GoogleMapController controller) { - controllerNotifier.value = controller; + controllerCompleter.complete(controller); }, initialCameraPosition: const CameraPosition( target: LatLng(0, 0), @@ -47,26 +41,25 @@ void main() { await tester.pump(); - final GoogleMapController? controller = controllerNotifier.value; + final GoogleMapController? controller = await controllerCompleter.future; - if (controller != null) { - expect(platform.mapEventStreamController.hasListener, true); + if (controller == null) { + fail('GoogleMapController not created'); + } - await tester.pumpWidget(const Directionality( - textDirection: TextDirection.ltr, - child: SizedBox(), - )); + expect(platform.mapEventStreamController.hasListener, true); - await tester.binding.runAsync(() async { - await tester.pump(); - }); + // Remove the map from the widget tree. + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.ltr, + child: SizedBox(), + )); - expect(platform.mapEventStreamController.hasListener, false); + await tester.binding.runAsync(() async { + await tester.pump(); + }); - controllerNotifier.dispose(); - } else { - fail('GoogleMapController not created'); - } + expect(platform.mapEventStreamController.hasListener, false); }); } From 651c98ad7e0970813b1529e0a3b01bc615854be6 Mon Sep 17 00:00:00 2001 From: 10909 Date: Wed, 23 Apr 2025 09:48:59 +0800 Subject: [PATCH 09/12] docs: update CHANGELOG file --- .../google_maps_flutter/google_maps_flutter/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 69001816bf2..73521c2a8e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,7 +1,11 @@ -## 2.12.2 +## 2.12.3 * Fixes memory leak by disposing stream subscriptions in `GoogleMapController`. +## 2.12.2 + +* Updates README to indicate that Andoid SDK <21 is no longer supported. + ## 2.12.1 * Fixes typo in README. From 8e9ab885585e3b1993c5bbf22996950baf639ba8 Mon Sep 17 00:00:00 2001 From: 10909 Date: Wed, 23 Apr 2025 10:06:51 +0800 Subject: [PATCH 10/12] refactor: format test file --- .../google_maps_flutter/test/controller_test.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart index 7aa3826650e..a279848a84b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/controller_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -62,14 +61,3 @@ void main() { expect(platform.mapEventStreamController.hasListener, false); }); } - -class FakePlatformViewsController { - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - return Future.value(1); - default: - return Future.value(); - } - } -} From fee9f73974f63853fd9efdd1d3d0251fee0fc5ad Mon Sep 17 00:00:00 2001 From: 10909 Date: Wed, 23 Apr 2025 10:15:49 +0800 Subject: [PATCH 11/12] docs: update version --- packages/google_maps_flutter/google_maps_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 974bbaa2101..c7126a047b4 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.12.2 +version: 2.12.3 environment: sdk: ^3.6.0 From 84418a86da25434cc8a9bef3174feef249ade3f3 Mon Sep 17 00:00:00 2001 From: 10909 Date: Wed, 23 Apr 2025 10:30:55 +0800 Subject: [PATCH 12/12] docs: update CHANGELOG and version --- .../google_maps_flutter/google_maps_flutter/CHANGELOG.md | 5 +---- .../google_maps_flutter/google_maps_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 73521c2a8e7..edc28239419 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,9 +1,6 @@ -## 2.12.3 - -* Fixes memory leak by disposing stream subscriptions in `GoogleMapController`. - ## 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 diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index c7126a047b4..974bbaa2101 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.12.3 +version: 2.12.2 environment: sdk: ^3.6.0