Unverified Commit cb0a613e authored by Polina Cherkasova's avatar Polina Cherkasova Committed by GitHub

SegmentedButton should not create new MaterialStatesController in every build. (#133949)

parent 85bece26
......@@ -96,7 +96,7 @@ class ButtonSegment<T> {
/// [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<T> extends StatelessWidget {
class SegmentedButton<T> extends StatefulWidget {
/// Creates a const [SegmentedButton].
///
/// [segments] must contain at least one segment, but it is recommended
......@@ -235,27 +235,33 @@ class SegmentedButton<T> extends StatelessWidget {
/// Defaults to an [Icon] with [Icons.check].
final Widget? selectedIcon;
bool get _enabled => onSelectionChanged != null;
@override
State<SegmentedButton<T>> createState() => _SegmentedButtonState<T>();
}
class _SegmentedButtonState<T> extends State<SegmentedButton<T>> {
bool get _enabled => widget.onSelectionChanged != null;
final Map<ButtonSegment<T>, MaterialStatesController> _statesControllers = <ButtonSegment<T>, 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<T> pressedSegment = <T>{segmentValue};
late final Set<T> 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<T> extends StatelessWidget {
final Set<MaterialState> currentState = _enabled ? enabledState : disabledState;
P? effectiveValue<P>(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<T> 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<T> 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(
<MaterialState>{
final MaterialStatesController controller = _statesControllers.putIfAbsent(segment, () => MaterialStatesController());
controller.value = <MaterialState>{
if (segmentSelected) MaterialState.selected,
}
);
};
final Widget button = icon != null
? TextButton.icon(
......@@ -350,7 +355,7 @@ class SegmentedButton<T> 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<T> extends StatelessWidget {
final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide);
final OutlinedBorder disabledBorder = resolvedDisabledBorder.copyWith(side: disabledSide);
final List<Widget> buttons = segments.map(buttonFor).toList();
final List<Widget> buttons = widget.segments.map(buttonFor).toList();
return Material(
type: MaterialType.transparency,
......@@ -374,7 +379,7 @@ class SegmentedButton<T> extends StatelessWidget {
child: TextButtonTheme(
data: TextButtonThemeData(style: segmentThemeStyle),
child: _SegmentedButtonRenderWidget<T>(
segments: segments,
segments: widget.segments,
enabledBorder: _enabled ? enabledBorder : disabledBorder,
disabledBorder: disabledBorder,
direction: direction,
......@@ -383,6 +388,14 @@ class SegmentedButton<T> extends StatelessWidget {
),
);
}
@override
void dispose() {
for (final MaterialStatesController controller in _statesControllers.values) {
controller.dispose();
}
super.dispose();
}
}
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
const _SegmentedButtonRenderWidget({
......
......@@ -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(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment