From 799aa15405b4361d599cdf52057331a3f9f09a32 Mon Sep 17 00:00:00 2001 From: gregpakes Date: Sun, 1 Nov 2026 16:43:52 +0000 Subject: [PATCH] fix: dispose keyboard leaping notifiers MapInteractiveViewerState created _isPanLeaping, _isZoomLeaping and _isRotateLeaping but never disposed them, so leak_tracker reports every map. They are disposed after the keyboard animations, because those drive the listeners that write them. Co-Authored-By: Claude Opus 5.5 --- lib/src/gestures/map_interactive_viewer.dart | 3 ++ .../gestures/map_interactive_viewer_test.dart | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index 3d1e89e3b..47d90db06 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -182,6 +182,9 @@ class MapInteractiveViewerState extends State _keyboardListenerFocusNode.dispose(); } _disposeKeyboardAnimations(); + _isPanLeaping.dispose(); + _isZoomLeaping.dispose(); + _isRotateLeaping.dispose(); super.dispose(); } diff --git a/test/gestures/map_interactive_viewer_test.dart b/test/gestures/map_interactive_viewer_test.dart index b7ee28728..271a31a1e 100644 --- a/test/gestures/map_interactive_viewer_test.dart +++ b/test/gestures/map_interactive_viewer_test.dart @@ -1,3 +1,7 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/gestures/map_interactive_viewer.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -46,4 +50,54 @@ void main() { }, ); }); + + group('MapInteractiveViewerState.dispose', () { + Widget map() => const Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: MediaQueryData(size: Size(800, 600)), + child: FlutterMap(children: []), + ), + ); + + testWidgets('disposes every ValueNotifier it created', + (tester) async { + final created = {}; + final disposed = {}; + void onEvent(ObjectEvent event) { + if (event.object is! ValueNotifier) return; + (event is ObjectCreated ? created : disposed).add(event.object); + } + + FlutterMemoryAllocations.instance.addListener(onEvent); + addTearDown( + () => FlutterMemoryAllocations.instance.removeListener(onEvent), + ); + + await tester.pumpWidget(map()); + await tester.pumpWidget(const SizedBox.shrink()); + + expect(created, isNotEmpty); + expect(created.difference(disposed), isEmpty); + }); + + testWidgets('can be torn down while a keyboard leap is in progress', + (tester) async { + await tester.pumpWidget(map()); + await tester.pump(); + + // A short press starts a leap, which writes the leaping notifiers from + // the animation's listener until the curve reaches its leap maximum. + await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(const Duration(milliseconds: 16)); + await tester.pump(const Duration(milliseconds: 16)); + await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(const Duration(milliseconds: 16)); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(seconds: 1)); + + expect(tester.takeException(), isNull); + }); + }); }