Skip to content

Commit cbf35b4

Browse files
Fix memory leaks in navigation rail (#146988)
1 parent fb110b9 commit cbf35b4

File tree

2 files changed

+104
-65
lines changed

2 files changed

+104
-65
lines changed

packages/flutter/lib/src/material/navigation_rail.dart

Lines changed: 99 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ class _NavigationRailState extends State<NavigationRail> with TickerProviderStat
346346
late List<AnimationController> _destinationControllers;
347347
late List<Animation<double>> _destinationAnimations;
348348
late AnimationController _extendedController;
349-
late Animation<double> _extendedAnimation;
349+
late CurvedAnimation _extendedAnimation;
350350

351351
@override
352352
void initState() {
@@ -488,6 +488,8 @@ class _NavigationRailState extends State<NavigationRail> with TickerProviderStat
488488
controller.dispose();
489489
}
490490
_extendedController.dispose();
491+
_extendedAnimation.dispose();
492+
491493
}
492494

493495
void _initControllers() {
@@ -528,8 +530,8 @@ class _NavigationRailState extends State<NavigationRail> with TickerProviderStat
528530
}
529531
}
530532

531-
class _RailDestination extends StatelessWidget {
532-
_RailDestination({
533+
class _RailDestination extends StatefulWidget {
534+
const _RailDestination({
533535
required this.minWidth,
534536
required this.minExtendedWidth,
535537
required this.icon,
@@ -547,11 +549,7 @@ class _RailDestination extends StatelessWidget {
547549
this.indicatorColor,
548550
this.indicatorShape,
549551
this.disabled = false,
550-
}) : _positionAnimation = CurvedAnimation(
551-
parent: ReverseAnimation(destinationAnimation),
552-
curve: Curves.easeInOut,
553-
reverseCurve: Curves.easeInOut.flipped,
554-
);
552+
});
555553

556554
final double minWidth;
557555
final double minExtendedWidth;
@@ -571,111 +569,148 @@ class _RailDestination extends StatelessWidget {
571569
final ShapeBorder? indicatorShape;
572570
final bool disabled;
573571

574-
final Animation<double> _positionAnimation;
572+
573+
@override
574+
State<_RailDestination> createState() => _RailDestinationState();
575+
}
576+
577+
class _RailDestinationState extends State<_RailDestination> {
578+
late CurvedAnimation _positionAnimation;
579+
580+
@override
581+
void initState() {
582+
super.initState();
583+
_setPositionAnimation();
584+
}
585+
586+
@override
587+
void didUpdateWidget(_RailDestination oldWidget) {
588+
super.didUpdateWidget(oldWidget);
589+
if (widget.destinationAnimation != oldWidget.destinationAnimation) {
590+
_positionAnimation.dispose();
591+
_setPositionAnimation();
592+
}
593+
}
594+
595+
void _setPositionAnimation() {
596+
_positionAnimation = CurvedAnimation(
597+
parent: ReverseAnimation(widget.destinationAnimation),
598+
curve: Curves.easeInOut,
599+
reverseCurve: Curves.easeInOut.flipped,
600+
);
601+
}
602+
603+
@override
604+
void dispose() {
605+
_positionAnimation.dispose();
606+
super.dispose();
607+
}
608+
609+
575610

576611
@override
577612
Widget build(BuildContext context) {
578613
assert(
579-
useIndicator || indicatorColor == null,
614+
widget.useIndicator || widget.indicatorColor == null,
580615
'[NavigationRail.indicatorColor] does not have an effect when [NavigationRail.useIndicator] is false',
581616
);
582617

583618
final ThemeData theme = Theme.of(context);
584619
final TextDirection textDirection = Directionality.of(context);
585620
final bool material3 = theme.useMaterial3;
586-
final EdgeInsets destinationPadding = (padding ?? EdgeInsets.zero).resolve(textDirection);
621+
final EdgeInsets destinationPadding = (widget.padding ?? EdgeInsets.zero).resolve(textDirection);
587622
Offset indicatorOffset;
588623
bool applyXOffset = false;
589624

590625
final Widget themedIcon = IconTheme(
591-
data: disabled
592-
? iconTheme.copyWith(color: theme.colorScheme.onSurface.withOpacity(0.38))
593-
: iconTheme,
594-
child: icon,
626+
data: widget.disabled
627+
? widget.iconTheme.copyWith(color: theme.colorScheme.onSurface.withOpacity(0.38))
628+
: widget.iconTheme,
629+
child: widget.icon,
595630
);
596631
final Widget styledLabel = DefaultTextStyle(
597-
style: disabled
598-
? labelTextStyle.copyWith(color: theme.colorScheme.onSurface.withOpacity(0.38))
599-
: labelTextStyle,
600-
child: label,
632+
style: widget.disabled
633+
? widget.labelTextStyle.copyWith(color: theme.colorScheme.onSurface.withOpacity(0.38))
634+
: widget.labelTextStyle,
635+
child: widget.label,
601636
);
602637

603638
Widget content;
604639

605640
// The indicator height is fixed and equal to _kIndicatorHeight.
606641
// When the icon height is larger than the indicator height the indicator
607642
// vertical offset is used to vertically center the indicator.
608-
final bool isLargeIconSize = iconTheme.size != null && iconTheme.size! > _kIndicatorHeight;
609-
final double indicatorVerticalOffset = isLargeIconSize ? (iconTheme.size! - _kIndicatorHeight) / 2 : 0;
643+
final bool isLargeIconSize = widget.iconTheme.size != null && widget.iconTheme.size! > _kIndicatorHeight;
644+
final double indicatorVerticalOffset = isLargeIconSize ? (widget.iconTheme.size! - _kIndicatorHeight) / 2 : 0;
610645

611-
switch (labelType) {
646+
switch (widget.labelType) {
612647
case NavigationRailLabelType.none:
613648
// Split the destination spacing across the top and bottom to keep the icon centered.
614649
final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null;
615650
indicatorOffset = Offset(
616-
minWidth / 2 + destinationPadding.left,
651+
widget.minWidth / 2 + destinationPadding.left,
617652
_verticalDestinationSpacingM3 / 2 + destinationPadding.top + indicatorVerticalOffset,
618653
);
619654
final Widget iconPart = Column(
620655
children: <Widget>[
621656
if (spacing != null) spacing,
622657
SizedBox(
623-
width: minWidth,
624-
height: material3 ? null : minWidth,
658+
width: widget.minWidth,
659+
height: material3 ? null : widget.minWidth,
625660
child: Center(
626661
child: _AddIndicator(
627-
addIndicator: useIndicator,
628-
indicatorColor: indicatorColor,
629-
indicatorShape: indicatorShape,
662+
addIndicator: widget.useIndicator,
663+
indicatorColor: widget.indicatorColor,
664+
indicatorShape: widget.indicatorShape,
630665
isCircular: !material3,
631-
indicatorAnimation: destinationAnimation,
666+
indicatorAnimation: widget.destinationAnimation,
632667
child: themedIcon,
633668
),
634669
),
635670
),
636671
if (spacing != null) spacing,
637672
],
638673
);
639-
if (extendedTransitionAnimation.value == 0) {
674+
if (widget.extendedTransitionAnimation.value == 0) {
640675
content = Padding(
641-
padding: padding ?? EdgeInsets.zero,
676+
padding: widget.padding ?? EdgeInsets.zero,
642677
child: Stack(
643678
children: <Widget>[
644679
iconPart,
645680
// For semantics when label is not showing,
646681
SizedBox.shrink(
647682
child: Visibility.maintain(
648683
visible: false,
649-
child: label,
684+
child: widget.label,
650685
),
651686
),
652687
],
653688
),
654689
);
655690
} else {
656-
final Animation<double> labelFadeAnimation = extendedTransitionAnimation.drive(CurveTween(curve: const Interval(0.0, 0.25)));
691+
final Animation<double> labelFadeAnimation = widget.extendedTransitionAnimation.drive(CurveTween(curve: const Interval(0.0, 0.25)));
657692
applyXOffset = true;
658693
content = Padding(
659-
padding: padding ?? EdgeInsets.zero,
694+
padding: widget.padding ?? EdgeInsets.zero,
660695
child: ConstrainedBox(
661696
constraints: BoxConstraints(
662-
minWidth: lerpDouble(minWidth, minExtendedWidth, extendedTransitionAnimation.value)!,
697+
minWidth: lerpDouble(widget.minWidth, widget.minExtendedWidth, widget.extendedTransitionAnimation.value)!,
663698
),
664699
child: ClipRect(
665700
child: Row(
666701
children: <Widget>[
667702
iconPart,
668703
Align(
669704
heightFactor: 1.0,
670-
widthFactor: extendedTransitionAnimation.value,
705+
widthFactor: widget.extendedTransitionAnimation.value,
671706
alignment: AlignmentDirectional.centerStart,
672707
child: FadeTransition(
673708
alwaysIncludeSemantics: true,
674709
opacity: labelFadeAnimation,
675710
child: styledLabel,
676711
),
677712
),
678-
SizedBox(width: _horizontalDestinationPadding * extendedTransitionAnimation.value),
713+
SizedBox(width: _horizontalDestinationPadding * widget.extendedTransitionAnimation.value),
679714
],
680715
),
681716
),
@@ -685,42 +720,42 @@ class _RailDestination extends StatelessWidget {
685720
case NavigationRailLabelType.selected:
686721
final double appearingAnimationValue = 1 - _positionAnimation.value;
687722
final double verticalPadding = lerpDouble(_verticalDestinationPaddingNoLabel, _verticalDestinationPaddingWithLabel, appearingAnimationValue)!;
688-
final Interval interval = selected ? const Interval(0.25, 0.75) : const Interval(0.75, 1.0);
689-
final Animation<double> labelFadeAnimation = destinationAnimation.drive(CurveTween(curve: interval));
690-
final double minHeight = material3 ? 0 : minWidth;
723+
final Interval interval = widget.selected ? const Interval(0.25, 0.75) : const Interval(0.75, 1.0);
724+
final Animation<double> labelFadeAnimation = widget.destinationAnimation.drive(CurveTween(curve: interval));
725+
final double minHeight = material3 ? 0 : widget.minWidth;
691726
final Widget topSpacing = SizedBox(height: material3 ? 0 : verticalPadding);
692727
final Widget labelSpacing = SizedBox(height: material3 ? lerpDouble(0, _verticalIconLabelSpacingM3, appearingAnimationValue)! : 0);
693728
final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : verticalPadding);
694729
final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2);
695730
final double indicatorVerticalPadding = destinationPadding.top;
696731
indicatorOffset = Offset(
697-
minWidth / 2 + indicatorHorizontalPadding,
732+
widget.minWidth / 2 + indicatorHorizontalPadding,
698733
indicatorVerticalPadding + indicatorVerticalOffset,
699734
);
700-
if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
735+
if (widget.minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
701736
indicatorOffset = Offset(
702-
minWidth / 2 + _horizontalDestinationSpacingM3,
737+
widget.minWidth / 2 + _horizontalDestinationSpacingM3,
703738
indicatorVerticalPadding + indicatorVerticalOffset,
704739
);
705740
}
706741
content = Container(
707742
constraints: BoxConstraints(
708-
minWidth: minWidth,
743+
minWidth: widget.minWidth,
709744
minHeight: minHeight,
710745
),
711-
padding: padding ?? const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
746+
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
712747
child: ClipRect(
713748
child: Column(
714749
mainAxisSize: MainAxisSize.min,
715750
mainAxisAlignment: MainAxisAlignment.center,
716751
children: <Widget>[
717752
topSpacing,
718753
_AddIndicator(
719-
addIndicator: useIndicator,
720-
indicatorColor: indicatorColor,
721-
indicatorShape: indicatorShape,
754+
addIndicator: widget.useIndicator,
755+
indicatorColor: widget.indicatorColor,
756+
indicatorShape: widget.indicatorShape,
722757
isCircular: false,
723-
indicatorAnimation: destinationAnimation,
758+
indicatorAnimation: widget.destinationAnimation,
724759
child: themedIcon,
725760
),
726761
labelSpacing,
@@ -740,37 +775,37 @@ class _RailDestination extends StatelessWidget {
740775
),
741776
);
742777
case NavigationRailLabelType.all:
743-
final double minHeight = material3 ? 0 : minWidth;
778+
final double minHeight = material3 ? 0 : widget.minWidth;
744779
final Widget topSpacing = SizedBox(height: material3 ? 0 : _verticalDestinationPaddingWithLabel);
745780
final Widget labelSpacing = SizedBox(height: material3 ? _verticalIconLabelSpacingM3 : 0);
746781
final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel);
747782
final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2);
748783
final double indicatorVerticalPadding = destinationPadding.top;
749784
indicatorOffset = Offset(
750-
minWidth / 2 + indicatorHorizontalPadding,
785+
widget.minWidth / 2 + indicatorHorizontalPadding,
751786
indicatorVerticalPadding + indicatorVerticalOffset,
752787
);
753-
if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
788+
if (widget.minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
754789
indicatorOffset = Offset(
755-
minWidth / 2 + _horizontalDestinationSpacingM3,
790+
widget.minWidth / 2 + _horizontalDestinationSpacingM3,
756791
indicatorVerticalPadding + indicatorVerticalOffset,
757792
);
758793
}
759794
content = Container(
760795
constraints: BoxConstraints(
761-
minWidth: minWidth,
796+
minWidth: widget.minWidth,
762797
minHeight: minHeight,
763798
),
764-
padding: padding ?? const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
799+
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
765800
child: Column(
766801
children: <Widget>[
767802
topSpacing,
768803
_AddIndicator(
769-
addIndicator: useIndicator,
770-
indicatorColor: indicatorColor,
771-
indicatorShape: indicatorShape,
804+
addIndicator: widget.useIndicator,
805+
indicatorColor: widget.indicatorColor,
806+
indicatorShape: widget.indicatorShape,
772807
isCircular: false,
773-
indicatorAnimation: destinationAnimation,
808+
indicatorAnimation: widget.destinationAnimation,
774809
child: themedIcon,
775810
),
776811
labelSpacing,
@@ -791,15 +826,15 @@ class _RailDestination extends StatelessWidget {
791826
: colors.primary.withOpacity(0.04);
792827
return Semantics(
793828
container: true,
794-
selected: selected,
829+
selected: widget.selected,
795830
child: Stack(
796831
children: <Widget>[
797832
Material(
798833
type: MaterialType.transparency,
799834
child: _IndicatorInkWell(
800-
onTap: disabled ? null : onTap,
801-
borderRadius: BorderRadius.all(Radius.circular(minWidth / 2.0)),
802-
customBorder: indicatorShape,
835+
onTap: widget.disabled ? null : widget.onTap,
836+
borderRadius: BorderRadius.all(Radius.circular(widget.minWidth / 2.0)),
837+
customBorder: widget.indicatorShape,
803838
splashColor: effectiveSplashColor,
804839
hoverColor: effectiveHoverColor,
805840
useMaterial3: material3,
@@ -810,7 +845,7 @@ class _RailDestination extends StatelessWidget {
810845
),
811846
),
812847
Semantics(
813-
label: indexLabel,
848+
label: widget.indexLabel,
814849
),
815850
],
816851
),

packages/flutter/test/material/navigation_rail_theme_test.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:flutter/material.dart';
66
import 'package:flutter/rendering.dart';
77
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
89

910
void main() {
1011
test('copyWith, ==, hashCode basics', () {
@@ -145,7 +146,10 @@ void main() {
145146
expect(_indicatorDecoration(tester)?.shape, indicatorShape);
146147
});
147148

148-
testWidgets('NavigationRail values take priority over NavigationRailThemeData values when both properties are specified', (WidgetTester tester) async {
149+
testWidgets('NavigationRail values take priority over NavigationRailThemeData values when both properties are specified',
150+
// TODO(polina-c): remove when fixed https://github.com/flutter/flutter/issues/145600 [leak-tracking-opt-in]
151+
experimentalLeakTesting: LeakTesting.settings.withTracked(classes: const <String>['CurvedAnimation']),
152+
(WidgetTester tester) async {
149153
const Color backgroundColor = Color(0x00000001);
150154
const double elevation = 7.0;
151155
const double selectedIconSize = 25.0;

0 commit comments

Comments
 (0)