Skip to content

Commit 2f75005

Browse files
author
Shi-Hao Hong
authored
Fix Exception on Nested TabBarView disposal (#31581)
* Add Flag to determine if pixels is set by viewport during disposal * Add TODO to remove nested TabBarView workaround once unnecessary build/dispose issues are resolved
1 parent d53115a commit 2f75005

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

packages/flutter/lib/src/widgets/page_view.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,29 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
251251
final int initialPage;
252252
double _pageToUseOnStartup;
253253

254+
/// If [pixels] isn't set by [applyViewportDimension] before [dispose] is
255+
/// called, this could throw an assert as [pixels] will be set to null.
256+
///
257+
/// With [Tab]s, this happens when there are nested [TabBarView]s and there
258+
/// is an attempt to warp over the nested tab to a tab adjacent to it.
259+
///
260+
/// This flag will be set to true once the dimensions have been established
261+
/// and [pixels] is set.
262+
bool isInitialPixelsValueSet = false;
263+
264+
@override
265+
void dispose() {
266+
// TODO(shihaohong): remove workaround once these issues have been
267+
// resolved, https://github.com/flutter/flutter/issues/32054,
268+
// https://github.com/flutter/flutter/issues/32056
269+
// Sets `pixels` to a non-null value before `ScrollPosition.dispose` is
270+
// invoked if it was never set by `applyViewportDimension`.
271+
if (pixels == null && !isInitialPixelsValueSet) {
272+
correctPixels(0);
273+
}
274+
super.dispose();
275+
}
276+
254277
@override
255278
double get viewportFraction => _viewportFraction;
256279
double _viewportFraction;
@@ -295,8 +318,10 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
295318
final double oldPixels = pixels;
296319
final double page = (oldPixels == null || oldViewportDimensions == 0.0) ? _pageToUseOnStartup : getPageFromPixels(oldPixels, oldViewportDimensions);
297320
final double newPixels = getPixelsFromPage(page);
321+
298322
if (newPixels != oldPixels) {
299323
correctPixels(newPixels);
324+
isInitialPixelsValueSet = true;
300325
return false;
301326
}
302327
return result;

packages/flutter/test/material/tabs_test.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,42 @@ class AlwaysKeepAliveState extends State<AlwaysKeepAliveWidget>
6767
}
6868
}
6969

70+
class _NestedTabBarContainer extends StatelessWidget {
71+
const _NestedTabBarContainer({
72+
this.tabController,
73+
});
74+
75+
final TabController tabController;
76+
77+
@override
78+
Widget build(BuildContext context) {
79+
return Container(
80+
color: Colors.blue,
81+
child: Column(
82+
children: <Widget>[
83+
TabBar(
84+
controller: tabController,
85+
tabs: const <Tab>[
86+
Tab(text: 'Yellow'),
87+
Tab(text: 'Grey'),
88+
],
89+
),
90+
Expanded(
91+
flex: 1,
92+
child: TabBarView(
93+
controller: tabController,
94+
children: <Widget>[
95+
Container(color: Colors.yellow),
96+
Container(color: Colors.grey),
97+
],
98+
),
99+
)
100+
],
101+
),
102+
);
103+
}
104+
}
105+
70106
Widget buildFrame({
71107
Key tabBarKey,
72108
List<String> tabs,
@@ -942,6 +978,51 @@ void main() {
942978
expect(tabController.index, 0);
943979
});
944980

981+
testWidgets('Nested TabBarView sets ScrollController pixels to non-null value '
982+
'when disposed before it is set by the applyViewportDimension', (WidgetTester tester) async {
983+
// This is a regression test for https://github.com/flutter/flutter/issues/18756
984+
final TabController _mainTabController = TabController(length: 4, vsync: const TestVSync());
985+
final TabController _nestedTabController = TabController(length: 2, vsync: const TestVSync());
986+
987+
await tester.pumpWidget(
988+
MaterialApp(
989+
home: Scaffold(
990+
appBar: AppBar(
991+
title: const Text('Exception for Nested Tabs'),
992+
bottom: TabBar(
993+
controller: _mainTabController,
994+
tabs: const <Widget>[
995+
Tab(icon: Icon(Icons.add), text: 'A'),
996+
Tab(icon: Icon(Icons.add), text: 'B'),
997+
Tab(icon: Icon(Icons.add), text: 'C'),
998+
Tab(icon: Icon(Icons.add), text: 'D'),
999+
],
1000+
),
1001+
),
1002+
body: TabBarView(
1003+
controller: _mainTabController,
1004+
children: <Widget>[
1005+
Container(color: Colors.red),
1006+
_NestedTabBarContainer(tabController: _nestedTabController),
1007+
Container(color: Colors.green),
1008+
Container(color: Colors.indigo),
1009+
],
1010+
),
1011+
),
1012+
)
1013+
);
1014+
1015+
// expect first tab to be selected
1016+
expect(_mainTabController.index, 0);
1017+
1018+
// tap on third tab
1019+
await tester.tap(find.text('C'));
1020+
await tester.pumpAndSettle();
1021+
1022+
// expect third tab to be selected without exceptions
1023+
expect(_mainTabController.index, 2);
1024+
});
1025+
9451026
testWidgets('TabBarView scrolls end close to a new page with custom physics', (WidgetTester tester) async {
9461027
final TabController tabController = TabController(
9471028
vsync: const TestVSync(),

0 commit comments

Comments
 (0)