Skip to content

Commit 30234a0

Browse files
authored
Fix ExpansionTile properties cannot be updated with setState (#134218)
fixes [`ExpansionTile` properties aren't updated with `setState`](flutter/flutter#24493) ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @OverRide Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: Example(), ); } } class Example extends StatefulWidget { const Example({super.key}); @OverRide State<Example> createState() => _ExampleState(); } class _ExampleState extends State<Example> { ShapeBorder collapsedShape = const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4)), ); Color collapsedTextColor = const Color(0xffffffff); Color collapsedBackgroundColor = const Color(0xffff0000); Color collapsedIconColor = const Color(0xffffffff); ShapeBorder shape = const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ); Color backgroundColor = const Color(0xffff0000); Color textColor = const Color(0xffffffff); Color iconColor = const Color(0xffffffff); @OverRide Widget build(BuildContext context) { return Scaffold( body: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ExpansionTile( shape: shape, backgroundColor: backgroundColor, textColor: textColor, iconColor: iconColor, collapsedShape: collapsedShape, collapsedTextColor: collapsedTextColor, collapsedBackgroundColor: collapsedBackgroundColor, collapsedIconColor: collapsedIconColor, title: const Text('Collapsed ExpansionTile'), children: const [ ListTile( title: Text('Revealed!'), ), ], ), const SizedBox(height: 16), ExpansionTile( shape: shape, backgroundColor: backgroundColor, textColor: textColor, iconColor: iconColor, initiallyExpanded: true, title: const Text('Expanded ExpansionTile'), children: const [ ListTile( title: Text('Revealed!'), ), ], ), const SizedBox(height: 16), FilledButton( onPressed: () { setState(() { collapsedShape = const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(50)), ); collapsedTextColor = const Color(0xfff00000); collapsedBackgroundColor = const Color(0xffffff00); collapsedIconColor = const Color(0xfff00000); shape = const RoundedRectangleBorder(); backgroundColor = const Color(0xfffff000); textColor = const Color(0xfff00000); iconColor = const Color(0xfff00000); }); }, child: const Text('Update properties'), ), ], ), ), ), ); } } ``` </details> ### Before https://github.com/flutter/flutter/assets/48603081/b29aed98-38ff-40a3-9ed3-c4342ada35b6 ### After https://github.com/flutter/flutter/assets/48603081/5e0b6a34-c577-40ed-8456-7ef55caa277b
1 parent 2032fcc commit 30234a0

File tree

2 files changed

+199
-1
lines changed

2 files changed

+199
-1
lines changed

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,13 +668,47 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
668668
);
669669
}
670670

671+
@override
672+
void didUpdateWidget(covariant ExpansionTile oldWidget) {
673+
super.didUpdateWidget(oldWidget);
674+
final ThemeData theme = Theme.of(context);
675+
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
676+
final ExpansionTileThemeData defaults = theme.useMaterial3
677+
? _ExpansionTileDefaultsM3(context)
678+
: _ExpansionTileDefaultsM2(context);
679+
if (widget.collapsedShape != oldWidget.collapsedShape
680+
|| widget.shape != oldWidget.shape) {
681+
_updateShapeBorder(expansionTileTheme, theme);
682+
}
683+
if (widget.collapsedTextColor != oldWidget.collapsedTextColor
684+
|| widget.textColor != oldWidget.textColor) {
685+
_updateHeaderColor(expansionTileTheme, defaults);
686+
}
687+
if (widget.collapsedIconColor != oldWidget.collapsedIconColor
688+
|| widget.iconColor != oldWidget.iconColor) {
689+
_updateIconColor(expansionTileTheme, defaults);
690+
}
691+
if (widget.backgroundColor != oldWidget.backgroundColor
692+
|| widget.collapsedBackgroundColor != oldWidget.collapsedBackgroundColor) {
693+
_updateBackgroundColor(expansionTileTheme);
694+
}
695+
}
696+
671697
@override
672698
void didChangeDependencies() {
673699
final ThemeData theme = Theme.of(context);
674700
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
675701
final ExpansionTileThemeData defaults = theme.useMaterial3
676702
? _ExpansionTileDefaultsM3(context)
677703
: _ExpansionTileDefaultsM2(context);
704+
_updateShapeBorder(expansionTileTheme, theme);
705+
_updateHeaderColor(expansionTileTheme, defaults);
706+
_updateIconColor(expansionTileTheme, defaults);
707+
_updateBackgroundColor(expansionTileTheme);
708+
super.didChangeDependencies();
709+
}
710+
711+
void _updateShapeBorder(ExpansionTileThemeData expansionTileTheme, ThemeData theme) {
678712
_borderTween
679713
..begin = widget.collapsedShape
680714
?? expansionTileTheme.collapsedShape
@@ -688,20 +722,28 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
688722
top: BorderSide(color: theme.dividerColor),
689723
bottom: BorderSide(color: theme.dividerColor),
690724
);
725+
}
726+
727+
void _updateHeaderColor(ExpansionTileThemeData expansionTileTheme, ExpansionTileThemeData defaults) {
691728
_headerColorTween
692729
..begin = widget.collapsedTextColor
693730
?? expansionTileTheme.collapsedTextColor
694731
?? defaults.collapsedTextColor
695732
..end = widget.textColor ?? expansionTileTheme.textColor ?? defaults.textColor;
733+
}
734+
735+
void _updateIconColor(ExpansionTileThemeData expansionTileTheme, ExpansionTileThemeData defaults) {
696736
_iconColorTween
697737
..begin = widget.collapsedIconColor
698738
?? expansionTileTheme.collapsedIconColor
699739
?? defaults.collapsedIconColor
700740
..end = widget.iconColor ?? expansionTileTheme.iconColor ?? defaults.iconColor;
741+
}
742+
743+
void _updateBackgroundColor(ExpansionTileThemeData expansionTileTheme) {
701744
_backgroundColorTween
702745
..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor
703746
..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor;
704-
super.didChangeDependencies();
705747
}
706748

707749
@override

packages/flutter/test/material/expansion_tile_test.dart

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,162 @@ void main() {
893893
handle.dispose();
894894
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
895895

896+
testWidgetsWithLeakTracking('Collapsed ExpansionTile properties can be updated with setState', (WidgetTester tester) async {
897+
const Key expansionTileKey = Key('expansionTileKey');
898+
ShapeBorder collapsedShape = const RoundedRectangleBorder(
899+
borderRadius: BorderRadius.all(Radius.circular(4)),
900+
);
901+
Color collapsedTextColor = const Color(0xffffffff);
902+
Color collapsedBackgroundColor = const Color(0xffff0000);
903+
Color collapsedIconColor = const Color(0xffffffff);
904+
905+
await tester.pumpWidget(MaterialApp(
906+
home: Material(
907+
child: StatefulBuilder(
908+
builder: (BuildContext context, StateSetter setState) {
909+
return Column(
910+
children: <Widget>[
911+
ExpansionTile(
912+
key: expansionTileKey,
913+
collapsedShape: collapsedShape,
914+
collapsedTextColor: collapsedTextColor,
915+
collapsedBackgroundColor: collapsedBackgroundColor,
916+
collapsedIconColor: collapsedIconColor,
917+
title: const TestText('title'),
918+
trailing: const TestIcon(),
919+
children: const <Widget>[
920+
SizedBox(height: 100, width: 100),
921+
],
922+
),
923+
// This button is used to update the ExpansionTile properties.
924+
FilledButton(
925+
onPressed: () {
926+
setState(() {
927+
collapsedShape = const RoundedRectangleBorder(
928+
borderRadius: BorderRadius.all(Radius.circular(16)),
929+
);
930+
collapsedTextColor = const Color(0xff000000);
931+
collapsedBackgroundColor = const Color(0xffffff00);
932+
collapsedIconColor = const Color(0xff000000);
933+
});
934+
},
935+
child: const Text('Update collapsed properties'),
936+
),
937+
],
938+
);
939+
}
940+
),
941+
),
942+
));
943+
944+
ShapeDecoration shapeDecoration = tester.firstWidget<Container>(find.descendant(
945+
of: find.byKey(expansionTileKey),
946+
matching: find.byType(Container),
947+
)).decoration! as ShapeDecoration;
948+
949+
// Test initial ExpansionTile properties.
950+
expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))));
951+
expect(shapeDecoration.color, const Color(0xffff0000));
952+
expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xffffffff));
953+
expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xffffffff));
954+
955+
// Tap the button to update the ExpansionTile properties.
956+
await tester.tap(find.text('Update collapsed properties'));
957+
await tester.pumpAndSettle();
958+
959+
shapeDecoration = tester.firstWidget<Container>(find.descendant(
960+
of: find.byKey(expansionTileKey),
961+
matching: find.byType(Container),
962+
)).decoration! as ShapeDecoration;
963+
964+
// Test updated ExpansionTile properties.
965+
expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))));
966+
expect(shapeDecoration.color, const Color(0xffffff00));
967+
expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xff000000));
968+
expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xff000000));
969+
});
970+
971+
testWidgetsWithLeakTracking('Expanded ExpansionTile properties can be updated with setState', (WidgetTester tester) async {
972+
const Key expansionTileKey = Key('expansionTileKey');
973+
ShapeBorder shape = const RoundedRectangleBorder(
974+
borderRadius: BorderRadius.all(Radius.circular(12)),
975+
);
976+
Color textColor = const Color(0xff00ffff);
977+
Color backgroundColor = const Color(0xff0000ff);
978+
Color iconColor = const Color(0xff00ffff);
979+
980+
await tester.pumpWidget(MaterialApp(
981+
home: Material(
982+
child: StatefulBuilder(
983+
builder: (BuildContext context, StateSetter setState) {
984+
return Column(
985+
children: <Widget>[
986+
ExpansionTile(
987+
key: expansionTileKey,
988+
shape: shape,
989+
textColor: textColor,
990+
backgroundColor: backgroundColor,
991+
iconColor: iconColor,
992+
title: const TestText('title'),
993+
trailing: const TestIcon(),
994+
children: const <Widget>[
995+
SizedBox(height: 100, width: 100),
996+
],
997+
),
998+
// This button is used to update the ExpansionTile properties.
999+
FilledButton(
1000+
onPressed: () {
1001+
setState(() {
1002+
shape = const RoundedRectangleBorder(
1003+
borderRadius: BorderRadius.all(Radius.circular(6)),
1004+
);
1005+
textColor = const Color(0xffffffff);
1006+
backgroundColor = const Color(0xff123456);
1007+
iconColor = const Color(0xffffffff);
1008+
});
1009+
},
1010+
child: const Text('Update collapsed properties'),
1011+
),
1012+
],
1013+
);
1014+
}
1015+
),
1016+
),
1017+
));
1018+
1019+
// Tap to expand the ExpansionTile.
1020+
await tester.tap(find.text('title'));
1021+
await tester.pumpAndSettle();
1022+
1023+
ShapeDecoration shapeDecoration = tester.firstWidget<Container>(find.descendant(
1024+
of: find.byKey(expansionTileKey),
1025+
matching: find.byType(Container),
1026+
)).decoration! as ShapeDecoration;
1027+
1028+
// Test initial ExpansionTile properties.
1029+
expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))));
1030+
expect(shapeDecoration.color, const Color(0xff0000ff));
1031+
expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xff00ffff));
1032+
expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xff00ffff));
1033+
1034+
// Tap the button to update the ExpansionTile properties.
1035+
await tester.tap(find.text('Update collapsed properties'));
1036+
await tester.pumpAndSettle();
1037+
1038+
shapeDecoration = tester.firstWidget<Container>(find.descendant(
1039+
of: find.byKey(expansionTileKey),
1040+
matching: find.byType(Container),
1041+
)).decoration! as ShapeDecoration;
1042+
iconColor = tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!;
1043+
textColor = tester.state<TestTextState>(find.byType(TestText)).textStyle.color!;
1044+
1045+
// Test updated ExpansionTile properties.
1046+
expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6))));
1047+
expect(shapeDecoration.color, const Color(0xff123456));
1048+
expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xffffffff));
1049+
expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xffffffff));
1050+
});
1051+
8961052
group('Material 2', () {
8971053
// These tests are only relevant for Material 2. Once Material 2
8981054
// support is deprecated and the APIs are removed, these tests

0 commit comments

Comments
 (0)