Update TabBarView children after a transition to an adjacent tab (#112168)
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index 053671e..99f6102 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -1512,6 +1512,10 @@
_warpUnderwayCount += 1;
await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
_warpUnderwayCount -= 1;
+
+ if (mounted && widget.children != _children) {
+ setState(() { _updateChildren(); });
+ }
return Future<void>.value();
}
diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart
index 8c959e5..f87187d 100644
--- a/packages/flutter/test/material/tabs_test.dart
+++ b/packages/flutter/test/material/tabs_test.dart
@@ -859,20 +859,6 @@
expect(tabController.indexIsChanging, false);
});
- testWidgets('TabBarView child disposed during animation', (WidgetTester tester) async {
- // This is a regression test for the scenario brought up here
- // https://github.com/flutter/flutter/pull/7387#discussion_r95089191x
-
- final List<String> tabs = <String>['LEFT', 'RIGHT'];
- await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
-
- // Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
- final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
- await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
- await tester.pump();
- await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
- });
-
testWidgets('TabBar unselectedLabelColor control test', (WidgetTester tester) async {
final TabController controller = TabController(
vsync: const TestVSync(),
@@ -1563,6 +1549,95 @@
await tester.pump(const Duration(milliseconds: 300));
});
+
+ group('TabBarView children updated', () {
+
+ Widget buildFrameWithMarker(List<String> log, String marker) {
+ return MaterialApp(
+ home: DefaultTabController(
+ animationDuration: const Duration(seconds: 1),
+ length: 3,
+ child: Scaffold(
+ appBar: AppBar(
+ bottom: const TabBar(
+ tabs: <Widget>[
+ Tab(text: 'A'),
+ Tab(text: 'B'),
+ Tab(text: 'C'),
+ ],
+ ),
+ title: const Text('Tabs Test'),
+ ),
+ body: TabBarView(
+ children: <Widget>[
+ TabBody(index: 0, log: log, marker: marker),
+ TabBody(index: 1, log: log, marker: marker),
+ TabBody(index: 2, log: log, marker: marker),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ testWidgets('TabBarView children can be updated during animation to an adjacent tab', (WidgetTester tester) async {
+ // Regression test for https://github.com/flutter/flutter/issues/107399
+ final List<String> log = <String>[];
+
+ const String initialMarker = 'before';
+ await tester.pumpWidget(buildFrameWithMarker(log, initialMarker));
+ expect(log, <String>['init: 0']);
+ expect(find.text('0-$initialMarker'), findsOneWidget);
+
+ // Select the second tab and wait until the transition starts
+ await tester.tap(find.text('B'));
+ await tester.pump(const Duration(milliseconds: 100));
+
+ // Check that both TabBody's are instantiated while the transition is animating
+ await tester.pump(const Duration(milliseconds: 400));
+ expect(log, <String>['init: 0', 'init: 1']);
+
+ // Update the TabBody's states while the transition is animating
+ const String updatedMarker = 'after';
+ await tester.pumpWidget(buildFrameWithMarker(log, updatedMarker));
+
+ // Wait until the transition ends
+ await tester.pumpAndSettle();
+
+ // The TabBody state of the second TabBar should have been updated
+ expect(find.text('1-$initialMarker'), findsNothing);
+ expect(find.text('1-$updatedMarker'), findsOneWidget);
+ });
+
+ testWidgets('TabBarView children can be updated during animation to a non adjacent tab', (WidgetTester tester) async {
+ final List<String> log = <String>[];
+
+ const String initialMarker = 'before';
+ await tester.pumpWidget(buildFrameWithMarker(log, initialMarker));
+ expect(log, <String>['init: 0']);
+ expect(find.text('0-$initialMarker'), findsOneWidget);
+
+ // Select the third tab and wait until the transition starts
+ await tester.tap(find.text('C'));
+ await tester.pump(const Duration(milliseconds: 100));
+
+ // Check that both TabBody's are instantiated while the transition is animating
+ await tester.pump(const Duration(milliseconds: 400));
+ expect(log, <String>['init: 0', 'init: 2']);
+
+ // Update the TabBody's states while the transition is animating
+ const String updatedMarker = 'after';
+ await tester.pumpWidget(buildFrameWithMarker(log, updatedMarker));
+
+ // Wait until the transition ends
+ await tester.pumpAndSettle();
+
+ // The TabBody state of the third TabBar should have been updated
+ expect(find.text('2-$initialMarker'), findsNothing);
+ expect(find.text('2-$updatedMarker'), findsOneWidget);
+ });
+ });
+
testWidgets('TabBarView scrolls end close to a new page', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/9375
@@ -4976,10 +5051,11 @@
class MockScrollMetrics extends Fake implements ScrollMetrics { }
class TabBody extends StatefulWidget {
- const TabBody({ super.key, required this.index, required this.log });
+ const TabBody({ super.key, required this.index, required this.log, this.marker = '' });
final int index;
final List<String> log;
+ final String marker;
@override
State<TabBody> createState() => TabBodyState();
@@ -5008,7 +5084,9 @@
@override
Widget build(BuildContext context) {
return Center(
- child: Text('${widget.index}'),
+ child: widget.marker.isEmpty
+ ? Text('${widget.index}')
+ : Text('${widget.index}-${widget.marker}'),
);
}
}