Skip to content

Commit

Permalink
Do not crash if the controller and TabBarView are updated at differen…
Browse files Browse the repository at this point in the history
…t phases (build and layout) of the same frame. (#104998)
  • Loading branch information
xu-baolin authored Jun 6, 2022
1 parent e9230ba commit d73f7ad
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 18 deletions.
70 changes: 52 additions & 18 deletions packages/flutter/lib/src/material/tabs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,7 @@ class _TabBarState extends State<TabBar> {
int? _currentIndex;
late double _tabStripWidth;
late List<GlobalKey> _tabKeys;
bool _debugHasScheduledValidTabsCountCheck = false;

@override
void initState() {
Expand Down Expand Up @@ -1147,18 +1148,34 @@ class _TabBarState extends State<TabBar> {
);
}

bool _debugScheduleCheckHasValidTabsCount() {
if (_debugHasScheduledValidTabsCountCheck) {
return true;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
_debugHasScheduledValidTabsCountCheck = false;
if (!mounted) {
return;
}
assert(() {
if (_controller!.length != widget.tabs.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of tabs (${widget.tabs.length}) present in TabBar's tabs property.",
);
}
return true;
}());
});
_debugHasScheduledValidTabsCountCheck = true;
return true;
}

@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(() {
if (_controller!.length != widget.tabs.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of tabs (${widget.tabs.length}) present in TabBar's tabs property.",
);
}
return true;
}());
assert(_debugScheduleCheckHasValidTabsCount());

final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (_controller!.length == 0) {
return Container(
Expand Down Expand Up @@ -1375,6 +1392,7 @@ class _TabBarViewState extends State<TabBarView> {
late List<Widget> _childrenWithKey;
int? _currentIndex;
int _warpUnderwayCount = 0;
bool _debugHasScheduledValidChildrenCountCheck = false;

// If the TabBarView is rebuilt with a new tab controller, the caller should
// dispose the old one. In that case the old controller's animation will be
Expand Down Expand Up @@ -1550,17 +1568,33 @@ class _TabBarViewState extends State<TabBarView> {
return false;
}

bool _debugScheduleCheckHasValidChildrenCount() {
if (_debugHasScheduledValidChildrenCountCheck) {
return true;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
_debugHasScheduledValidChildrenCountCheck = false;
if (!mounted) {
return;
}
assert(() {
if (_controller!.length != widget.children.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of children (${widget.children.length}) present in TabBarView's children property.",
);
}
return true;
}());
});
_debugHasScheduledValidChildrenCountCheck = true;
return true;
}

@override
Widget build(BuildContext context) {
assert(() {
if (_controller!.length != widget.children.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of tabs (${widget.children.length}) present in TabBar's tabs property.",
);
}
return true;
}());
assert(_debugScheduleCheckHasValidChildrenCount());

return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PageView(
Expand Down
125 changes: 125 additions & 0 deletions packages/flutter/test/material/tabs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4617,6 +4617,131 @@ void main() {
);
gesture.removePointer();
});

testWidgets('Do not crash if the controller and TabBarView are updated at different phases(build and layout) of the same frame', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/104994.
List<String> tabTextContent = <String>[];

await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return DefaultTabController(
length: tabTextContent.length,
child: Scaffold(
appBar: AppBar(
title: const Text('Default TabBar Preview'),
bottom: tabTextContent.isNotEmpty
? TabBar(
isScrollable: true,
tabs: tabTextContent.map((String textContent) => Tab(text: textContent)).toList(),
)
: null,
),
body: LayoutBuilder(
builder: (_, __) {
return tabTextContent.isNotEmpty
? TabBarView(
children: tabTextContent.map((String textContent) => Tab(text: "$textContent's view")).toList(),
)
: const Center(child: Text('No tabs'));
},
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
key: const Key('Add tab'),
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
tabTextContent = List<String>.from(tabTextContent)
..add('Tab ${tabTextContent.length + 1}');
});
},
),
IconButton(
key: const Key('Delete tab'),
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
tabTextContent = List<String>.from(tabTextContent)
..removeLast();
});
},
),
],
),
),
),
);
},
),
),
);

// Initializes with zero tabs properly
expect(find.text('No tabs'), findsOneWidget);
await tester.tap(find.byKey(const Key('Add tab')));
await tester.pumpAndSettle();
expect(find.text('Tab 1'), findsOneWidget);
expect(find.text("Tab 1's view"), findsOneWidget);

// Dynamically updates to zero tabs properly
await tester.tap(find.byKey(const Key('Delete tab')));
await tester.pumpAndSettle();
expect(find.text('No tabs'), findsOneWidget);
});

testWidgets("Throw if the controller's length mismatch the tabs count", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: <Widget>[
Container(width: 100, height: 100, color: Colors.green),
],
),
),
),
),
),
);

expect(tester.takeException(), isAssertionError);
});

testWidgets("Throw if the controller's length mismatch the TabBarView‘s children count", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: DefaultTabController(
length: 1,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: <Widget>[
Container(width: 100, height: 100, color: Colors.green),
],
),
),
body: const TabBarView(
children: <Widget>[
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
),
),
);

expect(tester.takeException(), isAssertionError);
});
}

class KeepAliveInk extends StatefulWidget {
Expand Down

0 comments on commit d73f7ad

Please sign in to comment.