常见 Flutter 错误
如何识别并解决常见的 Flutter 框架错误。
介绍
#本页面介绍了几个在 Flutter 框架中经常遇到的错误(包括布局错误),并提供了解决建议。这是一个动态文档,未来修订版中会添加更多错误,欢迎大家贡献内容。请随时 提交 Issue 或 提交 Pull Request,帮助完善本页面,使其对您和 Flutter 社区更有价值。
运行应用时出现纯红色或灰色屏幕
#通常被称为“红屏(或灰屏)死机”,这是 Flutter 向您告知存在错误的一种方式。
红屏通常出现在以调试(debug)或分析(profile)模式运行应用时。灰屏则可能出现在以发布(release)模式运行应用时。
通常,这些错误是由未捕获的异常(您可能需要添加 try-catch 代码块)或某些渲染错误(例如溢出错误)引起的。
以下文章提供了有关调试此类错误的一些有用见解:
- Flutter 错误揭秘 (作者:Abishek)
- 理解并解决 Flutter 中的灰屏问题 (作者:Christopher Nwosu-Madueke)
- Flutter 卡在白屏界面 (作者:Kesar Bhimani)
“A RenderFlex overflowed…”(RenderFlex 溢出)
#RenderFlex 溢出是 Flutter 框架中最常见的错误之一,您可能已经遇到过它。
该错误是什么样的?
当它发生时,应用界面上会出现黄黑条纹,指示溢出的区域。此外,调试控制台中会显示错误消息。
The following assertion was thrown during layout:
A RenderFlex overflowed by 1146 pixels on the right.
The relevant error-causing widget was
Row lib/errors/renderflex_overflow_column.dart:23
The overflowing RenderFlex has an orientation of Axis.horizontal.
The edge of the RenderFlex that is overflowing has been marked in the rendering
with a yellow and black striped pattern. This is usually caused by the contents
being too big for the RenderFlex.
(Additional lines of this message omitted)
您可能会如何遇到此错误?
当 Column 或 Row 包含一个大小不受约束的子组件时,经常会出现此错误。例如,下面的代码片段演示了一个常见场景:
Widget build(BuildContext context) {
return Row(
children: [
const Icon(Icons.message),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
const Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed '
'do eiusmod tempor incididunt ut labore et dolore magna '
'aliqua. Ut enim ad minim veniam, quis nostrud '
'exercitation ullamco laboris nisi ut aliquip ex ea '
'commodo consequat.',
),
],
),
],
);
}
在上面的示例中,Column 试图占据比其父组件 Row 所能分配的空间更大的宽度,从而导致溢出错误。为什么 Column 会这样做?要理解这种布局行为,您需要了解 Flutter 框架是如何执行布局的。
“在执行布局时,Flutter 会以深度优先遍历方式遍历渲染树,并将大小约束从父级传递给子级……子级通过在父级建立的约束范围内向上传递一个大小来作为响应。” – Flutter 架构概览
在这种情况下,Row 小部件不约束其子项的大小,Column 小部件也不约束。由于缺少父小部件的约束,第二个 Text 小部件会尝试将其宽度设定为显示所有字符所需的宽度。随后,Column 采用了 Text 小部件自定的宽度,这与其父组件 Row 所能提供的最大水平空间发生了冲突。
如何修复?
您需要确保 Column 不会尝试超过其允许的宽度。为此,您需要限制其宽度。一种方法是用 Expanded 小部件包裹 Column。
return const Row(
children: [
Icon(Icons.message),
Expanded(
child: Column(
// code omitted
),
),
],
);
另一种方法是使用 Flexible 小部件包裹 Column 并指定 flex 系数。实际上,正如其源代码所示,Expanded 小部件等同于 flex 系数为 1.0 的 Flexible 小部件。要进一步了解如何在 Flutter 布局中使用 Flex 小部件,请查看关于 Flexible 小部件的 90 秒每周组件视频。
更多信息
以下链接资源提供了有关此错误的更多信息。
“RenderBox was not laid out”(RenderBox 未进行布局)
#虽然这个错误很常见,但它通常是渲染流水线中较早发生的原始错误的副作用。
该错误是什么样的?
该错误显示的消息如下所示:
RenderBox was not laid out:
RenderViewport#5a477 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
您可能会如何遇到此错误?
通常,此问题与违反框约束(box constraints)有关,需要通过向 Flutter 提供关于您希望如何约束相关小部件的更多信息来解决。您可以在 理解约束 页面了解 Flutter 中约束的工作原理。
RenderBox was not laid out 错误通常由另外两个错误中的一个引起:
- “Vertical viewport was given unbounded height”(垂直视口获得了无限高度)
- “An InputDecorator...cannot have an unbounded width”(InputDecorator 不能具有无限宽度)
“Vertical viewport was given unbounded height”(垂直视口获得了无限高度)
#这是在 Flutter 应用中构建 UI 时可能遇到的另一个常见布局错误。
该错误是什么样的?
该错误显示的消息如下所示:
The following assertion was thrown during performResize():
Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their container.
In this case, a vertical viewport was given an unlimited amount of
vertical space in which to expand. This situation typically happens when a
scrollable widget is nested inside another scrollable widget.
(Additional lines of this message omitted)
您可能会如何遇到此错误?
该错误通常发生于 ListView(或其他类型的可滚动小部件,如 GridView)被放置在 Column 内部时。除非受到父小部件的约束,否则 ListView 会占用所有可用的垂直空间。然而,Column 默认不对其子项的高度施加任何约束。这两种行为结合在一起,导致无法确定 ListView 的大小。
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
const Text('Header'),
ListView(
children: const <Widget>[
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
],
),
);
}
如何修复?
要修复此错误,请指定 ListView 应有的高度。要使其高度与 Column 中剩余空间一致,请使用 Expanded 小部件包裹它(如下例所示)。或者,使用 SizedBox 小部件指定绝对高度,或使用 Flexible 小部件指定相对高度。
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
const Text('Header'),
Expanded(
child: ListView(
children: const <Widget>[
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
),
],
),
);
}
更多信息
以下链接资源提供了有关此错误的更多信息。
“An InputDecorator...cannot have an unbounded width”(InputDecorator 不能具有无限宽度)
#错误消息表明它也与框约束有关,理解这一点对于避免许多常见的 Flutter 框架错误非常重要。
该错误是什么样的?
该错误显示的消息如下所示:
The following assertion was thrown during performLayout():
An InputDecorator, which is typically created by a TextField, cannot have an
unbounded width.
This happens when the parent widget does not provide a finite width constraint.
For example, if the InputDecorator is contained by a `Row`, then its width must
be constrained. An `Expanded` widget or a SizedBox can be used to constrain the
width of the InputDecorator or the TextField that contains it.
(Additional lines of this message omitted)
您可能会如何遇到此错误?
例如,当 Row 包含一个 TextFormField 或 TextField,但后者没有宽度约束时,就会出现此错误。
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Unbounded Width of the TextField')),
body: const Row(children: [TextField()]),
),
);
}
如何修复?
正如错误消息所建议的那样,通过使用 Expanded 或 SizedBox 小部件来约束文本字段以修复此错误。以下示例演示了如何使用 Expanded 小部件:
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Unbounded Width of the TextField')),
body: Row(children: [Expanded(child: TextFormField())]),
),
);
}
“Incorrect use of ParentData widget”(错误使用 ParentData 小部件)
#此错误涉及缺少预期的父组件。
该错误是什么样的?
该错误显示的消息如下所示:
The following assertion was thrown while looking for parent data:
Incorrect use of ParentDataWidget.
(Some lines of this message omitted)
Usually, this indicates that at least one of the offending ParentDataWidgets
listed above is not placed directly inside a compatible ancestor widget.
您可能会如何遇到此错误?
虽然 Flutter 的小部件在如何组合到 UI 中时通常非常灵活,但其中有一小部分小部件期望特定的父小部件。当您的组件树中无法满足此预期时,您很可能会遇到此错误。
以下是 Flutter 框架中期望特定父组件的小部件的不完整列表。欢迎提交 PR(使用页面右上角的文档图标)来扩充此列表。
| 小部件 | 预期的父组件 |
|---|---|
Flexible |
Row, Column, 或 Flex |
Expanded (一种特殊的 Flexible) |
Row, Column, 或 Flex |
Positioned | 层叠布局 |
TableCell | Table |
如何修复?
一旦您知道缺少哪个父组件,修复方法就显而易见了。
“setState called during build”(在构建期间调用了 setState)
#Flutter 代码中的 build 方法不是直接或间接调用 setState 的好地方。
该错误是什么样的?
当错误发生时,控制台会显示以下消息:
The following assertion was thrown building DialogPage(dirty, dependencies:
[_InheritedTheme, _LocalizationsScope-[GlobalKey#59a8e]],
state: _DialogPageState#f121e):
setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework
is already in the process of building widgets.
(Additional lines of this message omitted)
您可能会如何遇到此错误?
通常,此错误发生在 build 方法中调用 setState 方法时。
出现此错误的常见场景是尝试在 build 方法内触发 Dialog。这通常是出于立即向用户显示信息的需求,但永远不应在 build 方法中调用 setState。
以下代码片段似乎是此错误的常见元凶:
Widget build(BuildContext context) {
// Don't do this.
showDialog(
context: context,
builder: (context) {
return const AlertDialog(title: Text('Alert Dialog'));
},
);
return const Center(
child: Column(children: <Widget>[Text('Show Material Dialog')]),
);
}
此代码没有显式调用 setState,但它被 showDialog 调用了。build 方法不是调用 showDialog 的正确位置,因为框架可能会在每一帧都调用 build 方法,例如在动画期间。
如何修复?
避免此错误的一种方法是使用 Navigator API 将对话框作为路由触发。在下面的示例中,有两个页面。第二个页面有一个在进入时显示的对话框。当用户通过点击第一个页面上的按钮请求第二个页面时,Navigator 会推送两个路由——一个用于第二个页面,另一个用于对话框。
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Screen')),
body: Center(
child: ElevatedButton(
child: const Text('Launch screen'),
onPressed: () {
// Navigate to the second screen using a named route.
Navigator.pushNamed(context, '/second');
// Immediately show a dialog upon loading the second screen.
Navigator.push(
context,
PageRouteBuilder(
barrierDismissible: true,
opaque: false,
pageBuilder: (_, anim1, anim2) => const MyDialog(),
),
);
},
),
),
);
}
}
ScrollController 被附加到多个滚动视图
#
当屏幕上同时出现多个滚动小部件(例如 ListView)时,可能会发生此错误。相比移动应用,Web 或桌面应用更容易出现此错误,因为在移动设备上遇到这种情况很少见。
有关更多信息以及如何修复,请查看关于 PrimaryScrollController 的以下视频:
参考资料
#要了解更多关于如何调试错误(特别是 Flutter 中的布局错误)的信息,请查看以下资源: