Skip to content

Commit cb0a613

Browse files
authored
SegmentedButton should not create new MaterialStatesController in every build. (#133949)
1 parent 85bece2 commit cb0a613

File tree

2 files changed

+40
-24
lines changed

2 files changed

+40
-24
lines changed

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

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class ButtonSegment<T> {
9696
/// [ToggleButtons].
9797
/// * [Radio], an alternative way to present the user with a mutually exclusive set of options.
9898
/// * [FilterChip], [ChoiceChip], which can be used when you need to show more than five options.
99-
class SegmentedButton<T> extends StatelessWidget {
99+
class SegmentedButton<T> extends StatefulWidget {
100100
/// Creates a const [SegmentedButton].
101101
///
102102
/// [segments] must contain at least one segment, but it is recommended
@@ -235,27 +235,33 @@ class SegmentedButton<T> extends StatelessWidget {
235235
/// Defaults to an [Icon] with [Icons.check].
236236
final Widget? selectedIcon;
237237

238-
bool get _enabled => onSelectionChanged != null;
238+
@override
239+
State<SegmentedButton<T>> createState() => _SegmentedButtonState<T>();
240+
}
241+
242+
class _SegmentedButtonState<T> extends State<SegmentedButton<T>> {
243+
bool get _enabled => widget.onSelectionChanged != null;
244+
final Map<ButtonSegment<T>, MaterialStatesController> _statesControllers = <ButtonSegment<T>, MaterialStatesController>{};
239245

240246
void _handleOnPressed(T segmentValue) {
241247
if (!_enabled) {
242248
return;
243249
}
244-
final bool onlySelectedSegment = selected.length == 1 && selected.contains(segmentValue);
245-
final bool validChange = emptySelectionAllowed || !onlySelectedSegment;
250+
final bool onlySelectedSegment = widget.selected.length == 1 && widget.selected.contains(segmentValue);
251+
final bool validChange = widget.emptySelectionAllowed || !onlySelectedSegment;
246252
if (validChange) {
247-
final bool toggle = multiSelectionEnabled || (emptySelectionAllowed && onlySelectedSegment);
253+
final bool toggle = widget.multiSelectionEnabled || (widget.emptySelectionAllowed && onlySelectedSegment);
248254
final Set<T> pressedSegment = <T>{segmentValue};
249255
late final Set<T> updatedSelection;
250256
if (toggle) {
251-
updatedSelection = selected.contains(segmentValue)
252-
? selected.difference(pressedSegment)
253-
: selected.union(pressedSegment);
257+
updatedSelection = widget.selected.contains(segmentValue)
258+
? widget.selected.difference(pressedSegment)
259+
: widget.selected.union(pressedSegment);
254260
} else {
255261
updatedSelection = pressedSegment;
256262
}
257-
if (!setEquals(updatedSelection, selected)) {
258-
onSelectionChanged!(updatedSelection);
263+
if (!setEquals(updatedSelection, widget.selected)) {
264+
widget.onSelectionChanged!(updatedSelection);
259265
}
260266
}
261267
}
@@ -271,7 +277,7 @@ class SegmentedButton<T> extends StatelessWidget {
271277
final Set<MaterialState> currentState = _enabled ? enabledState : disabledState;
272278

273279
P? effectiveValue<P>(P? Function(ButtonStyle? style) getProperty) {
274-
late final P? widgetValue = getProperty(style);
280+
late final P? widgetValue = getProperty(widget.style);
275281
late final P? themeValue = getProperty(theme.style);
276282
late final P? defaultValue = getProperty(defaults.style);
277283
return widgetValue ?? themeValue ?? defaultValue;
@@ -305,25 +311,24 @@ class SegmentedButton<T> extends StatelessWidget {
305311
);
306312
}
307313

308-
final ButtonStyle segmentStyle = segmentStyleFor(style);
314+
final ButtonStyle segmentStyle = segmentStyleFor(widget.style);
309315
final ButtonStyle segmentThemeStyle = segmentStyleFor(theme.style).merge(segmentStyleFor(defaults.style));
310-
final Widget? selectedIcon = showSelectedIcon
311-
? this.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
316+
final Widget? selectedIcon = widget.showSelectedIcon
317+
? widget.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
312318
: null;
313319

314320
Widget buttonFor(ButtonSegment<T> segment) {
315321
final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink();
316-
final bool segmentSelected = selected.contains(segment.value);
317-
final Widget? icon = (segmentSelected && showSelectedIcon)
322+
final bool segmentSelected = widget.selected.contains(segment.value);
323+
final Widget? icon = (segmentSelected && widget.showSelectedIcon)
318324
? selectedIcon
319325
: segment.label != null
320326
? segment.icon
321327
: null;
322-
final MaterialStatesController controller = MaterialStatesController(
323-
<MaterialState>{
328+
final MaterialStatesController controller = _statesControllers.putIfAbsent(segment, () => MaterialStatesController());
329+
controller.value = <MaterialState>{
324330
if (segmentSelected) MaterialState.selected,
325-
}
326-
);
331+
};
327332

328333
final Widget button = icon != null
329334
? TextButton.icon(
@@ -350,7 +355,7 @@ class SegmentedButton<T> extends StatelessWidget {
350355
return MergeSemantics(
351356
child: Semantics(
352357
checked: segmentSelected,
353-
inMutuallyExclusiveGroup: multiSelectionEnabled ? null : true,
358+
inMutuallyExclusiveGroup: widget.multiSelectionEnabled ? null : true,
354359
child: buttonWithTooltip,
355360
),
356361
);
@@ -363,7 +368,7 @@ class SegmentedButton<T> extends StatelessWidget {
363368
final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide);
364369
final OutlinedBorder disabledBorder = resolvedDisabledBorder.copyWith(side: disabledSide);
365370

366-
final List<Widget> buttons = segments.map(buttonFor).toList();
371+
final List<Widget> buttons = widget.segments.map(buttonFor).toList();
367372

368373
return Material(
369374
type: MaterialType.transparency,
@@ -374,7 +379,7 @@ class SegmentedButton<T> extends StatelessWidget {
374379
child: TextButtonTheme(
375380
data: TextButtonThemeData(style: segmentThemeStyle),
376381
child: _SegmentedButtonRenderWidget<T>(
377-
segments: segments,
382+
segments: widget.segments,
378383
enabledBorder: _enabled ? enabledBorder : disabledBorder,
379384
disabledBorder: disabledBorder,
380385
direction: direction,
@@ -383,6 +388,14 @@ class SegmentedButton<T> extends StatelessWidget {
383388
),
384389
);
385390
}
391+
392+
@override
393+
void dispose() {
394+
for (final MaterialStatesController controller in _statesControllers.values) {
395+
controller.dispose();
396+
}
397+
super.dispose();
398+
}
386399
}
387400
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
388401
const _SegmentedButtonRenderWidget({

packages/flutter/test/material/segmented_button_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:ui';
99
import 'package:flutter/material.dart';
1010
import 'package:flutter/rendering.dart';
1111
import 'package:flutter_test/flutter_test.dart';
12+
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
1213

1314
import '../widgets/semantics_tester.dart';
1415

@@ -21,7 +22,9 @@ Widget boilerplate({required Widget child}) {
2122

2223
void main() {
2324

24-
testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', (WidgetTester tester) async {
25+
testWidgetsWithLeakTracking('SegmentedButton is built with Material of type MaterialType.transparency',
26+
leakTrackingTestConfig: LeakTrackingTestConfig.debugNotDisposed(),
27+
(WidgetTester tester) async {
2528
final ThemeData theme = ThemeData(useMaterial3: true);
2629
await tester.pumpWidget(
2730
MaterialApp(

0 commit comments

Comments
 (0)