From c855710af8ce87b6acaa92a7e87fc670c94a7361 Mon Sep 17 00:00:00 2001 From: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Date: Thu, 25 Apr 2024 23:55:23 +0800 Subject: [PATCH] Fix memory leaks in `Hero` widget (#147303) --- packages/flutter/lib/src/widgets/heroes.dart | 19 ++++++++++++++++--- .../flutter/test/widgets/heroes_test.dart | 5 ++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index 4ca022cb7b..8764da8d6b 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -425,7 +425,6 @@ class _HeroState extends State { } // Everything known about a hero flight that's to be started or diverted. -@immutable class _HeroFlightManifest { _HeroFlightManifest({ required this.type, @@ -455,8 +454,10 @@ class _HeroFlightManifest { Object get tag => fromHero.widget.tag; + CurvedAnimation? _animation; + Animation get animation { - return CurvedAnimation( + return _animation ??= CurvedAnimation( parent: (type == HeroFlightDirection.push) ? toRoute.animation! : fromRoute.animation!, curve: Curves.fastOutSlowIn, reverseCurve: isDiverted ? null : Curves.fastOutSlowIn.flipped, @@ -505,6 +506,11 @@ class _HeroFlightManifest { return '_HeroFlightManifest($type tag: $tag from route: ${fromRoute.settings} ' 'to route: ${toRoute.settings} with hero: $fromHero to $toHero)${isValid ? '' : ', INVALID'}'; } + + @mustCallSuper + void dispose() { + _animation?.dispose(); + } } // Builds the in-flight hero widget. @@ -531,7 +537,13 @@ class _HeroFlight { late ProxyAnimation _proxyAnimation; // The manifest will be available once `start` is called, throughout the // flight's lifecycle. - late _HeroFlightManifest manifest; + _HeroFlightManifest? _manifest; + _HeroFlightManifest get manifest => _manifest!; + set manifest (_HeroFlightManifest value) { + _manifest?.dispose(); + _manifest = value; + } + OverlayEntry? overlayEntry; bool _aborted = false; @@ -634,6 +646,7 @@ class _HeroFlight { _proxyAnimation.removeListener(onTick); _proxyAnimation.removeStatusListener(_handleAnimationUpdate); } + _manifest?.dispose(); } void onTick() { diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index d9bd88e1fc..9076c843b0 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -301,7 +301,10 @@ Future main() async { expect(find.byKey(thirdKey), isInCard); }); - testWidgets('Heroes still animate after hero controller is swapped.', (WidgetTester tester) async { + testWidgets('Heroes still animate after hero controller is swapped.', + // TODO(polina-c): remove when fixed https://github.com/flutter/flutter/issues/145600 [leak-tracking-opt-in] + experimentalLeakTesting: LeakTesting.settings.withTracked(classes: const ['CurvedAnimation']), + (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final UniqueKey heroKey = UniqueKey(); final HeroController controller1 = HeroController();