From f314f1bccc410014fda577507e949b7bbd7f6027 Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Fri, 23 Sep 2022 17:38:12 +0200 Subject: [PATCH] Update TabBarView children after a transition to an adjacent tab (#112168) --- packages/flutter/lib/src/material/tabs.dart | 4 + packages/flutter/test/material/tabs_test.dart | 110 +++++++++++++++--- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 053671e77a0..99f61020999 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -1512,6 +1512,10 @@ class _TabBarViewState extends State { _warpUnderwayCount += 1; await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease); _warpUnderwayCount -= 1; + + if (mounted && widget.children != _children) { + setState(() { _updateChildren(); }); + } return Future.value(); } diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 8c959e55629..f87187d7ccd 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -859,20 +859,6 @@ void main() { 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 tabs = ['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 @@ void main() { await tester.pump(const Duration(milliseconds: 300)); }); + + group('TabBarView children updated', () { + + Widget buildFrameWithMarker(List log, String marker) { + return MaterialApp( + home: DefaultTabController( + animationDuration: const Duration(seconds: 1), + length: 3, + child: Scaffold( + appBar: AppBar( + bottom: const TabBar( + tabs: [ + Tab(text: 'A'), + Tab(text: 'B'), + Tab(text: 'C'), + ], + ), + title: const Text('Tabs Test'), + ), + body: TabBarView( + children: [ + 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 log = []; + + const String initialMarker = 'before'; + await tester.pumpWidget(buildFrameWithMarker(log, initialMarker)); + expect(log, ['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, ['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 log = []; + + const String initialMarker = 'before'; + await tester.pumpWidget(buildFrameWithMarker(log, initialMarker)); + expect(log, ['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, ['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 TabBarDemo extends StatelessWidget { 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 log; + final String marker; @override State createState() => TabBodyState(); @@ -5008,7 +5084,9 @@ class TabBodyState extends State { @override Widget build(BuildContext context) { return Center( - child: Text('${widget.index}'), + child: widget.marker.isEmpty + ? Text('${widget.index}') + : Text('${widget.index}-${widget.marker}'), ); } }