diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 07531e2de1..d7d28f9463 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -251,6 +251,29 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri final int initialPage; double _pageToUseOnStartup; + /// If [pixels] isn't set by [applyViewportDimension] before [dispose] is + /// called, this could throw an assert as [pixels] will be set to null. + /// + /// With [Tab]s, this happens when there are nested [TabBarView]s and there + /// is an attempt to warp over the nested tab to a tab adjacent to it. + /// + /// This flag will be set to true once the dimensions have been established + /// and [pixels] is set. + bool isInitialPixelsValueSet = false; + + @override + void dispose() { + // TODO(shihaohong): remove workaround once these issues have been + // resolved, https://github.com/flutter/flutter/issues/32054, + // https://github.com/flutter/flutter/issues/32056 + // Sets `pixels` to a non-null value before `ScrollPosition.dispose` is + // invoked if it was never set by `applyViewportDimension`. + if (pixels == null && !isInitialPixelsValueSet) { + correctPixels(0); + } + super.dispose(); + } + @override double get viewportFraction => _viewportFraction; double _viewportFraction; @@ -295,8 +318,10 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri final double oldPixels = pixels; final double page = (oldPixels == null || oldViewportDimensions == 0.0) ? _pageToUseOnStartup : getPageFromPixels(oldPixels, oldViewportDimensions); final double newPixels = getPixelsFromPage(page); + if (newPixels != oldPixels) { correctPixels(newPixels); + isInitialPixelsValueSet = true; return false; } return result; diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 5f95e1cb92..8677c5e422 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -67,6 +67,42 @@ class AlwaysKeepAliveState extends State } } +class _NestedTabBarContainer extends StatelessWidget { + const _NestedTabBarContainer({ + this.tabController, + }); + + final TabController tabController; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.blue, + child: Column( + children: [ + TabBar( + controller: tabController, + tabs: const [ + Tab(text: 'Yellow'), + Tab(text: 'Grey'), + ], + ), + Expanded( + flex: 1, + child: TabBarView( + controller: tabController, + children: [ + Container(color: Colors.yellow), + Container(color: Colors.grey), + ], + ), + ) + ], + ), + ); + } +} + Widget buildFrame({ Key tabBarKey, List tabs, @@ -942,6 +978,51 @@ void main() { expect(tabController.index, 0); }); + testWidgets('Nested TabBarView sets ScrollController pixels to non-null value ' + 'when disposed before it is set by the applyViewportDimension', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/18756 + final TabController _mainTabController = TabController(length: 4, vsync: const TestVSync()); + final TabController _nestedTabController = TabController(length: 2, vsync: const TestVSync()); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Exception for Nested Tabs'), + bottom: TabBar( + controller: _mainTabController, + tabs: const [ + Tab(icon: Icon(Icons.add), text: 'A'), + Tab(icon: Icon(Icons.add), text: 'B'), + Tab(icon: Icon(Icons.add), text: 'C'), + Tab(icon: Icon(Icons.add), text: 'D'), + ], + ), + ), + body: TabBarView( + controller: _mainTabController, + children: [ + Container(color: Colors.red), + _NestedTabBarContainer(tabController: _nestedTabController), + Container(color: Colors.green), + Container(color: Colors.indigo), + ], + ), + ), + ) + ); + + // expect first tab to be selected + expect(_mainTabController.index, 0); + + // tap on third tab + await tester.tap(find.text('C')); + await tester.pumpAndSettle(); + + // expect third tab to be selected without exceptions + expect(_mainTabController.index, 2); + }); + testWidgets('TabBarView scrolls end close to a new page with custom physics', (WidgetTester tester) async { final TabController tabController = TabController( vsync: const TestVSync(),