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> {
int? _currentIndex;
late double _tabStripWidth;
late List<GlobalKey> _tabKeys;
bool _debugHasScheduledValidTabsCountCheck = false;
@override
void initState() {
......@@ -1147,9 +1148,15 @@ class _TabBarState extends State<TabBar> {
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
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(
......@@ -1159,6 +1166,16 @@ class _TabBarState extends State<TabBar> {
}
return true;
}());
});
_debugHasScheduledValidTabsCountCheck = true;
return true;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(_debugScheduleCheckHasValidTabsCount());
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (_controller!.length == 0) {
return Container(
......@@ -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
......@@ -1550,17 +1568,33 @@ class _TabBarViewState extends State<TabBarView> {
return false;
}
@override
Widget build(BuildContext context) {
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 tabs (${widget.children.length}) present in TabBar's tabs property.",
"number of children (${widget.children.length}) present in TabBarView's children property.",
);
}
return true;
}());
});
_debugHasScheduledValidChildrenCountCheck = true;
return true;
}
@override
Widget build(BuildContext context) {
assert(_debugScheduleCheckHasValidChildrenCount());
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PageView(
......
......@@ -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 {
......
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