diff --git a/packages/flutter/lib/src/cupertino/tab_view.dart b/packages/flutter/lib/src/cupertino/tab_view.dart index f41d0a4a31..1d38eb0bf7 100644 --- a/packages/flutter/lib/src/cupertino/tab_view.dart +++ b/packages/flutter/lib/src/cupertino/tab_view.dart @@ -156,6 +156,12 @@ class _CupertinoTabViewState extends State { } } + @override + void dispose() { + _heroController.dispose(); + super.dispose(); + } + void _updateObservers() { _navigatorObservers = List.of(widget.navigatorObservers) diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index 72a1716f51..2a8d3abe6f 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -784,7 +784,17 @@ class HeroController extends NavigatorObserver { /// /// The [createRectTween] argument is optional. If null, the controller uses a /// linear [Tween]. - HeroController({ this.createRectTween }); + HeroController({ this.createRectTween }) { + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'package:flutter/widgets.dart', + className: '$HeroController', + object: this, + ); + } + } /// Used to create [RectTween]s that interpolate the position of heroes in flight. /// @@ -1043,6 +1053,12 @@ class HeroController extends NavigatorObserver { /// Releases resources. @mustCallSuper void dispose() { + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } + for (final _HeroFlight flight in _flights.values) { flight.dispose(); } diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 19f3923478..e676c30115 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -1060,6 +1060,7 @@ void main() { testWidgetsWithLeakTracking('MaterialApp does create HeroController with the MaterialRectArcTween', (WidgetTester tester) async { final HeroController controller = MaterialApp.createMaterialHeroController(); + addTearDown(controller.dispose); final Tween tween = controller.createRectTween!( const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0), diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 46d26c1ea1..444df55245 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -304,9 +304,12 @@ Future main() async { testWidgetsWithLeakTracking('Heroes still animate after hero controller is swapped.', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final UniqueKey heroKey = UniqueKey(); + final HeroController controller1 = HeroController(); + addTearDown(controller1.dispose); + await tester.pumpWidget( HeroControllerScope( - controller: HeroController(), + controller: controller1, child: TestDependencies( child: Navigator( key: key, @@ -352,15 +355,19 @@ Future main() async { ); }, )); + expect(find.byKey(heroKey), findsNothing); // Begins the navigation await tester.pump(); await tester.pump(const Duration(milliseconds: 30)); expect(find.byKey(heroKey), isOnstage); + final HeroController controller2 = HeroController(); + addTearDown(controller2.dispose); + // Pumps a new hero controller. await tester.pumpWidget( HeroControllerScope( - controller: HeroController(), + controller: controller2, child: TestDependencies( child: Navigator( key: key, @@ -389,6 +396,7 @@ Future main() async { ), ), ); + // The original animation still flies. expect(find.byKey(heroKey), isOnstage); // Waits for the animation finishes. @@ -3135,6 +3143,13 @@ Future main() async { await tester.pumpAndSettle(); expect(tester.getTopLeft(find.byType(Image)).dy, moreOrLessEquals(forwardRest, epsilon: 0.1)); }); + + test('HeroController dispatches memory events', () async { + await expectLater( + await memoryEvents(() => HeroController().dispose(), HeroController), + areCreateAndDispose, + ); + }); } class TestDependencies extends StatelessWidget { diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index e01d3aa280..83139cf60d 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -2367,6 +2367,8 @@ void main() { ), ); }; + addTearDown(spy.dispose); + await tester.pumpWidget( HeroControllerScope( controller: spy, @@ -2437,6 +2439,8 @@ void main() { ), ); }; + addTearDown(spy.dispose); + await tester.pumpWidget( HeroControllerScope( controller: spy, @@ -2506,6 +2510,7 @@ void main() { ), ); }; + addTearDown(spy1.dispose); final List observations2 = []; final HeroControllerSpy spy2 = HeroControllerSpy() ..onPushed = (Route? route, Route? previousRoute) { @@ -2517,6 +2522,8 @@ void main() { ), ); }; + addTearDown(spy2.dispose); + await tester.pumpWidget( TestDependencies( child: Stack( @@ -2633,6 +2640,8 @@ void main() { testWidgetsWithLeakTracking('hero controller subscribes to multiple navigators does throw', (WidgetTester tester) async { final HeroControllerSpy spy = HeroControllerSpy(); + addTearDown(spy.dispose); + await tester.pumpWidget( HeroControllerScope( controller: spy, @@ -2671,6 +2680,8 @@ void main() { testWidgetsWithLeakTracking('hero controller throws has correct error message', (WidgetTester tester) async { final HeroControllerSpy spy = HeroControllerSpy(); + addTearDown(spy.dispose); + await tester.pumpWidget( HeroControllerScope( controller: spy,