diff --git a/packages/flutter/lib/src/material/segmented_button.dart b/packages/flutter/lib/src/material/segmented_button.dart index 1e23716b90..ea513282bd 100644 --- a/packages/flutter/lib/src/material/segmented_button.dart +++ b/packages/flutter/lib/src/material/segmented_button.dart @@ -96,7 +96,7 @@ class ButtonSegment { /// [ToggleButtons]. /// * [Radio], an alternative way to present the user with a mutually exclusive set of options. /// * [FilterChip], [ChoiceChip], which can be used when you need to show more than five options. -class SegmentedButton extends StatelessWidget { +class SegmentedButton extends StatefulWidget { /// Creates a const [SegmentedButton]. /// /// [segments] must contain at least one segment, but it is recommended @@ -235,27 +235,33 @@ class SegmentedButton extends StatelessWidget { /// Defaults to an [Icon] with [Icons.check]. final Widget? selectedIcon; - bool get _enabled => onSelectionChanged != null; + @override + State> createState() => _SegmentedButtonState(); +} + +class _SegmentedButtonState extends State> { + bool get _enabled => widget.onSelectionChanged != null; + final Map, MaterialStatesController> _statesControllers = , MaterialStatesController>{}; void _handleOnPressed(T segmentValue) { if (!_enabled) { return; } - final bool onlySelectedSegment = selected.length == 1 && selected.contains(segmentValue); - final bool validChange = emptySelectionAllowed || !onlySelectedSegment; + final bool onlySelectedSegment = widget.selected.length == 1 && widget.selected.contains(segmentValue); + final bool validChange = widget.emptySelectionAllowed || !onlySelectedSegment; if (validChange) { - final bool toggle = multiSelectionEnabled || (emptySelectionAllowed && onlySelectedSegment); + final bool toggle = widget.multiSelectionEnabled || (widget.emptySelectionAllowed && onlySelectedSegment); final Set pressedSegment = {segmentValue}; late final Set updatedSelection; if (toggle) { - updatedSelection = selected.contains(segmentValue) - ? selected.difference(pressedSegment) - : selected.union(pressedSegment); + updatedSelection = widget.selected.contains(segmentValue) + ? widget.selected.difference(pressedSegment) + : widget.selected.union(pressedSegment); } else { updatedSelection = pressedSegment; } - if (!setEquals(updatedSelection, selected)) { - onSelectionChanged!(updatedSelection); + if (!setEquals(updatedSelection, widget.selected)) { + widget.onSelectionChanged!(updatedSelection); } } } @@ -271,7 +277,7 @@ class SegmentedButton extends StatelessWidget { final Set currentState = _enabled ? enabledState : disabledState; P? effectiveValue

(P? Function(ButtonStyle? style) getProperty) { - late final P? widgetValue = getProperty(style); + late final P? widgetValue = getProperty(widget.style); late final P? themeValue = getProperty(theme.style); late final P? defaultValue = getProperty(defaults.style); return widgetValue ?? themeValue ?? defaultValue; @@ -305,25 +311,24 @@ class SegmentedButton extends StatelessWidget { ); } - final ButtonStyle segmentStyle = segmentStyleFor(style); + final ButtonStyle segmentStyle = segmentStyleFor(widget.style); final ButtonStyle segmentThemeStyle = segmentStyleFor(theme.style).merge(segmentStyleFor(defaults.style)); - final Widget? selectedIcon = showSelectedIcon - ? this.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon + final Widget? selectedIcon = widget.showSelectedIcon + ? widget.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon : null; Widget buttonFor(ButtonSegment segment) { final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink(); - final bool segmentSelected = selected.contains(segment.value); - final Widget? icon = (segmentSelected && showSelectedIcon) + final bool segmentSelected = widget.selected.contains(segment.value); + final Widget? icon = (segmentSelected && widget.showSelectedIcon) ? selectedIcon : segment.label != null ? segment.icon : null; - final MaterialStatesController controller = MaterialStatesController( - { + final MaterialStatesController controller = _statesControllers.putIfAbsent(segment, () => MaterialStatesController()); + controller.value = { if (segmentSelected) MaterialState.selected, - } - ); + }; final Widget button = icon != null ? TextButton.icon( @@ -350,7 +355,7 @@ class SegmentedButton extends StatelessWidget { return MergeSemantics( child: Semantics( checked: segmentSelected, - inMutuallyExclusiveGroup: multiSelectionEnabled ? null : true, + inMutuallyExclusiveGroup: widget.multiSelectionEnabled ? null : true, child: buttonWithTooltip, ), ); @@ -363,7 +368,7 @@ class SegmentedButton extends StatelessWidget { final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide); final OutlinedBorder disabledBorder = resolvedDisabledBorder.copyWith(side: disabledSide); - final List buttons = segments.map(buttonFor).toList(); + final List buttons = widget.segments.map(buttonFor).toList(); return Material( type: MaterialType.transparency, @@ -374,7 +379,7 @@ class SegmentedButton extends StatelessWidget { child: TextButtonTheme( data: TextButtonThemeData(style: segmentThemeStyle), child: _SegmentedButtonRenderWidget( - segments: segments, + segments: widget.segments, enabledBorder: _enabled ? enabledBorder : disabledBorder, disabledBorder: disabledBorder, direction: direction, @@ -383,6 +388,14 @@ class SegmentedButton extends StatelessWidget { ), ); } + + @override + void dispose() { + for (final MaterialStatesController controller in _statesControllers.values) { + controller.dispose(); + } + super.dispose(); + } } class _SegmentedButtonRenderWidget extends MultiChildRenderObjectWidget { const _SegmentedButtonRenderWidget({ diff --git a/packages/flutter/test/material/segmented_button_test.dart b/packages/flutter/test/material/segmented_button_test.dart index 57b31588e8..cb302dc3cd 100644 --- a/packages/flutter/test/material/segmented_button_test.dart +++ b/packages/flutter/test/material/segmented_button_test.dart @@ -9,6 +9,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; @@ -21,7 +22,9 @@ Widget boilerplate({required Widget child}) { void main() { - testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton is built with Material of type MaterialType.transparency', + leakTrackingTestConfig: LeakTrackingTestConfig.debugNotDisposed(), + (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp(