介绍

#

此页面解释了几个经常遇到的 Flutter 框架错误(包括布局错误)并提供了解决它们的建议。这是一个动态文档,将在未来的修订版中添加更多错误,并且欢迎您的贡献。欢迎您 打开一个 issue提交一个 pull request,以使此页面对您和 Flutter 社区更有用。

运行应用时出现纯红色或灰色屏幕

#

通常被称为“红色(或灰色)死亡屏幕”,有时 Flutter 会通过这种方式告知您存在错误。

当应用程序在调试或 profile 模式下运行时,可能会出现红色屏幕。当应用程序在 release 模式下运行时,可能会出现灰色屏幕。

通常,这些错误发生在未捕获的异常(您可能需要另一个 try-catch 块)或某些渲染错误(如溢出错误)时。

以下文章提供了一些关于调试此类错误的有用见解

'RenderFlex 溢出…'

#

RenderFlex 溢出是 Flutter 框架中最常遇到的错误之一,您可能已经遇到过它了。

错误看起来是怎样的?

当它发生时,会出现黄色和黑色的条纹,指示应用程序 UI 中溢出的区域。此外,会在调试控制台中显示错误消息。

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)

您可能会如何遇到此错误?

ColumnRow 包含一个大小未受限的子控件时,通常会发生此错误。例如,以下代码片段展示了一个常见场景

dart
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 控件会尝试与其需要显示的字符一样宽。Text 控件的自定宽度随后被 Column 采用,这与 Row 控件(其父控件)可以提供的最大水平空间发生冲突。

如何修复?

好吧,您需要确保 Column 不会尝试比其可用空间更宽。要实现这一点,您需要限制其宽度。一种方法是将 Column 包装在 Expanded 控件中。

dart
return const Row(
  children: [
    Icon(Icons.message),
    Expanded(
      child: Column(
        // code omitted
      ),
    ),
  ],
);

另一种方法是将 Column 包装在 Flexible 控件中并指定一个 flex 因子。事实上,Expanded 控件等同于 flex 因子为 1.0 的 Flexible 控件,正如其 源代码 所显示的那样。要进一步了解如何在 Flutter 布局中使用 Flex 控件,请查看 这个 90 秒的 Widget of the Week 视频,该视频介绍了 Flexible 控件。

更多信息

以下链接的资源提供了有关此错误的更多信息。

'RenderBox 未进行布局'

#

虽然此错误很常见,但它通常是渲染管道早期发生的某个主要错误的副作用。

错误看起来是怎样的?

错误显示的消息如下所示

RenderBox was not laid out: 
RenderViewport#5a477 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE

您可能会如何遇到此错误?

通常,该问题与违反盒子约束有关,需要通过向 Flutter 提供有关您希望如何约束相关控件的更多信息来解决。您可以在 理解约束 页面上了解有关 Flutter 中约束工作原理的更多信息。

RenderBox 未进行布局 错误通常由以下两个其他错误之一引起

  • 'Vertical viewport 被赋予了无界高度'
  • 'InputDecorator… 无法拥有无界宽度'

'Vertical viewport 被赋予了无界高度'

#

这是您在 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 的大小。

dart
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 控件指定相对高度。

dart
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')),
            ],
          ),
        ),
      ],
    ),
  );
}

更多信息

以下链接的资源提供了有关此错误的更多信息。

'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 包含 TextFormFieldTextField,但后者没有宽度约束时,就会发生此错误。

dart
Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      appBar: AppBar(title: const Text('Unbounded Width of the TextField')),
      body: const Row(children: [TextField()]),
    ),
  );
}

如何修复?

如错误消息所示,通过使用 ExpandedSizedBox 控件来约束文本字段来修复此错误。以下示例演示了使用 Expanded 控件。

dart
Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      appBar: AppBar(title: const Text('Unbounded Width of the TextField')),
      body: Row(children: [Expanded(child: TextFormField())]),
    ),
  );
}

'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(使用页面右上角的 doc 图标)来扩展此列表。

小部件期望的父控件
FlexibleRowColumnFlex
Expanded(一个特殊的 FlexibleRowColumnFlex
Positioned层叠布局
TableCellTable

如何修复?

一旦您知道缺少哪个父控件,修复应该就很明显了。

'setState 在 build 过程中被调用'

#

在 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)

您可能会如何遇到此错误?

总的来说,当 setState 方法在 build 方法中被调用时,就会发生此错误。

此错误发生的一个常见场景是在 build 方法中尝试触发 Dialog。这通常是因为需要立即向用户显示信息,但 setState 永远不应从 build 方法调用。

以下代码片段似乎是此错误的常见原因

dart
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 会推送两个路由——一个用于第二个页面,另一个用于对话框。

dart
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 的视频

在 YouTube 上在新标签页中观看:“PrimaryScrollController | Decoding Flutter”

参考资料

#

要了解有关如何调试错误(尤其是在 Flutter 中进行布局错误)的更多信息,请查看以下资源