diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index 9e3086f4124..a107ccbfa85 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -94,7 +94,7 @@ typedef ExpansionTileController = ExpansibleController; /// {@end-tool} /// /// {@tool dartpad} -/// This example demonstrates how an [ExpansionTileController] can be used to +/// This example demonstrates how an [ExpansibleController] can be used to /// programmatically expand or collapse an [ExpansionTile]. /// /// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.1.dart ** @@ -398,7 +398,7 @@ class ExpansionTile extends StatefulWidget { /// In cases where control over the tile's state is needed from a callback /// triggered by a widget within the tile, [ExpansibleController.of] may be /// more convenient than supplying a controller. - final ExpansionTileController? controller; + final ExpansibleController? controller; /// {@macro flutter.material.ListTile.dense} final bool? dense; @@ -497,7 +497,7 @@ class _ExpansionTileState extends State { late Animation _backgroundColor; late ExpansionTileThemeData _expansionTileTheme; - late ExpansionTileController _tileController; + late ExpansibleController _tileController; Timer? _timer; late Curve _curve; late Curve? _reverseCurve; @@ -508,7 +508,7 @@ class _ExpansionTileState extends State { super.initState(); _curve = Curves.easeIn; _duration = _kExpand; - _tileController = widget.controller ?? ExpansionTileController(); + _tileController = widget.controller ?? ExpansibleController(); if (widget.initiallyExpanded) { _tileController.expand(); } @@ -718,6 +718,15 @@ class _ExpansionTileState extends State { _updateAnimationDuration(); _updateHeightFactorCurve(); } + if (widget.controller != oldWidget.controller) { + _tileController.removeListener(_onExpansionChanged); + if (oldWidget.controller == null) { + _tileController.dispose(); + } + + _tileController = widget.controller ?? ExpansibleController(); + _tileController.addListener(_onExpansionChanged); + } } @override diff --git a/packages/flutter/lib/src/widgets/expansible.dart b/packages/flutter/lib/src/widgets/expansible.dart index 9dfb2546488..9bef1854859 100644 --- a/packages/flutter/lib/src/widgets/expansible.dart +++ b/packages/flutter/lib/src/widgets/expansible.dart @@ -329,6 +329,9 @@ class _ExpansibleState extends State with SingleTickerProviderStateM if (widget.controller != oldWidget.controller) { oldWidget.controller.removeListener(_toggleExpansion); widget.controller.addListener(_toggleExpansion); + if (oldWidget.controller.isExpanded != widget.controller.isExpanded) { + _toggleExpansion(); + } } } diff --git a/packages/flutter/test/material/expansion_tile_test.dart b/packages/flutter/test/material/expansion_tile_test.dart index 40b8a8efb36..a73a1ece581 100644 --- a/packages/flutter/test/material/expansion_tile_test.dart +++ b/packages/flutter/test/material/expansion_tile_test.dart @@ -1744,4 +1744,106 @@ void main() { final Offset offsetPlatform = tester.getTopLeft(platform); expect(offsetPlatform, const Offset(16.0, 17.0)); }); + + testWidgets('ExpansionTile can accept a new controller', (WidgetTester tester) async { + final ExpansibleController controller1 = ExpansibleController(); + final ExpansibleController controller2 = ExpansibleController(); + addTearDown(() { + controller1.dispose(); + controller2.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ExpansionTile( + controller: controller1, + title: const Text('Title'), + initiallyExpanded: true, + children: const [Text('Child 0')], + ), + ), + ), + ); + + expect(find.text('Child 0'), findsOne); + expect(controller1.isExpanded, isTrue); + controller1.collapse(); + expect(controller1.isExpanded, isFalse); + await tester.pumpAndSettle(); + expect(find.text('Child 0'), findsNothing); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ExpansionTile( + controller: controller2, + title: const Text('Title'), + initiallyExpanded: true, + children: const [Text('Child 0')], + ), + ), + ), + ); + + expect(find.text('Child 0'), findsNothing); + controller2.expand(); + expect(controller2.isExpanded, isTrue); + await tester.pumpAndSettle(); + expect(find.text('Child 0'), findsOne); + }); + + testWidgets('ExpansionTile can accept a new controller with a different state', ( + WidgetTester tester, + ) async { + final ExpansibleController controller1 = ExpansibleController(); + final ExpansibleController controller2 = ExpansibleController(); + addTearDown(() { + controller1.dispose(); + controller2.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ExpansionTile( + controller: controller1, + title: const Text('Title'), + children: const [Text('Child 0')], + ), + ), + ), + ); + + expect(find.text('Child 0'), findsNothing); + expect(controller1.isExpanded, isFalse); + controller1.expand(); + expect(controller1.isExpanded, isTrue); + await tester.pumpAndSettle(); + expect(find.text('Child 0'), findsOne); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ExpansionTile( + controller: controller2, + title: const Text('Title'), + children: const [Text('Child 0')], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect( + find.text('Child 0'), + findsNothing, + reason: 'The widget should update to the state of the new controller', + ); + controller2.expand(); + expect(controller2.isExpanded, isTrue); + await tester.pumpAndSettle(); + expect(find.text('Child 0'), findsOne); + }); } diff --git a/packages/flutter/test/widgets/expansible_test.dart b/packages/flutter/test/widgets/expansible_test.dart index c38be103a5d..340731eaedf 100644 --- a/packages/flutter/test/widgets/expansible_test.dart +++ b/packages/flutter/test/widgets/expansible_test.dart @@ -247,4 +247,120 @@ void main() { controller.dispose(); }); + + testWidgets('ExpansionTile can accept a new controller', (WidgetTester tester) async { + final ExpansibleController controller1 = ExpansibleController(); + final ExpansibleController controller2 = ExpansibleController(); + addTearDown(() { + controller1.dispose(); + controller2.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Expansible( + controller: controller1, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), + ), + ), + ), + ); + + expect(find.text('Body'), findsNothing); + expect(controller1.isExpanded, isFalse); + + controller1.expand(); + expect(controller1.isExpanded, isTrue); + await tester.pumpAndSettle(); + expect(find.text('Body'), findsOne); + + controller1.collapse(); + expect(controller1.isExpanded, isFalse); + await tester.pumpAndSettle(); + expect(find.text('Body'), findsNothing); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Expansible( + controller: controller2, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), + ), + ), + ), + ); + + expect(find.text('Body'), findsNothing); + expect(controller2.isExpanded, isFalse); + + controller2.expand(); + expect(controller2.isExpanded, isTrue); + await tester.pumpAndSettle(); + expect(find.text('Body'), findsOne); + + controller2.collapse(); + expect(controller2.isExpanded, isFalse); + await tester.pumpAndSettle(); + expect(find.text('Body'), findsNothing); + }); + + testWidgets('Expansible can accept a new controller with a different state', ( + WidgetTester tester, + ) async { + final ExpansibleController controller1 = ExpansibleController(); + final ExpansibleController controller2 = ExpansibleController(); + addTearDown(() { + controller1.dispose(); + controller2.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Expansible( + controller: controller1, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), + ), + ), + ), + ); + + expect(find.text('Body'), findsNothing); + expect(controller1.isExpanded, isFalse); + + controller1.expand(); + expect(controller1.isExpanded, isTrue); + await tester.pumpAndSettle(); + expect(find.text('Body'), findsOne); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Expansible( + controller: controller2, + headerBuilder: (_, _) => const Text('Header'), + bodyBuilder: (_, _) => const Text('Body'), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(controller2.isExpanded, isFalse); + expect(find.text('Body'), findsNothing); + + controller2.expand(); + expect(controller2.isExpanded, isTrue); + await tester.pumpAndSettle(); + expect(find.text('Body'), findsOne); + + controller2.collapse(); + expect(controller2.isExpanded, isFalse); + await tester.pumpAndSettle(); + expect(find.text('Body'), findsNothing); + }); }