Unverified Commit d73f7ad4 authored by xubaolin's avatar xubaolin Committed by GitHub

Do not crash if the controller and TabBarView are updated at different phases...

Do not crash if the controller and TabBarView are updated at different phases (build and layout) of the same frame. (#104998)
parent e9230bad
...@@ -914,6 +914,7 @@ class _TabBarState extends State<TabBar> { ...@@ -914,6 +914,7 @@ class _TabBarState extends State<TabBar> {
int? _currentIndex; int? _currentIndex;
late double _tabStripWidth; late double _tabStripWidth;
late List<GlobalKey> _tabKeys; late List<GlobalKey> _tabKeys;
bool _debugHasScheduledValidTabsCountCheck = false;
@override @override
void initState() { void initState() {
...@@ -1147,18 +1148,34 @@ class _TabBarState extends State<TabBar> { ...@@ -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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
assert(() { assert(_debugScheduleCheckHasValidTabsCount());
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;
}());
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (_controller!.length == 0) { if (_controller!.length == 0) {
return Container( return Container(
...@@ -1375,6 +1392,7 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1375,6 +1392,7 @@ class _TabBarViewState extends State<TabBarView> {
late List<Widget> _childrenWithKey; late List<Widget> _childrenWithKey;
int? _currentIndex; int? _currentIndex;
int _warpUnderwayCount = 0; int _warpUnderwayCount = 0;
bool _debugHasScheduledValidChildrenCountCheck = false;
// If the TabBarView is rebuilt with a new tab controller, the caller should // 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 // dispose the old one. In that case the old controller's animation will be
...@@ -1550,17 +1568,33 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1550,17 +1568,33 @@ class _TabBarViewState extends State<TabBarView> {
return false; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(() { assert(_debugScheduleCheckHasValidChildrenCount());
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;
}());
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification, onNotification: _handleScrollNotification,
child: PageView( child: PageView(
......
...@@ -4617,6 +4617,131 @@ void main() { ...@@ -4617,6 +4617,131 @@ void main() {
); );
gesture.removePointer(); 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 { class KeepAliveInk extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment