跳到主內容

常見 Flutter 錯誤

如何識別並解決常見的 Flutter 框架錯誤。

介紹

#

本頁面介紹了幾個在 Flutter 框架中經常遇到的錯誤(包括佈局錯誤),並提供瞭解決建議。這是一個動態文件,未來修訂版中會新增更多錯誤,歡迎大家貢獻內容。請隨時 提交 Issue提交 Pull Request,幫助完善本頁面,使其對您和 Flutter 社群更有價值。

執行應用時出現純紅色或灰色螢幕

#

通常被稱為“紅屏(或灰屏)宕機”,這是 Flutter 向您告知存在錯誤的一種方式。

紅屏通常出現在以除錯(debug)或分析(profile)模式執行應用時。灰屏則可能出現在以釋出(release)模式執行應用時。

通常,這些錯誤是由未捕獲的異常(您可能需要新增 try-catch 程式碼塊)或某些渲染錯誤(例如溢位錯誤)引起的。

以下文章提供了有關除錯此類錯誤的一些有用見解:

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

您可能會如何遇到此錯誤?

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 小部件會嘗試將其寬度設定為顯示所有字元所需的寬度。隨後,Column 採用了 Text 小部件自定的寬度,這與其父元件 Row 所能提供的最大水平空間發生了衝突。

如何修復?

您需要確保 Column 不會嘗試超過其允許的寬度。為此,您需要限制其寬度。一種方法是用 Expanded 小部件包裹 Column

dart
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 的大小。

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

更多資訊

以下連結資源提供了有關此錯誤的更多資訊。

“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 包含一個 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())]),
    ),
  );
}

“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層疊佈局
TableCellTable

如何修復?

一旦您知道缺少哪個父元件,修復方法就顯而易見了。

“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

以下程式碼片段似乎是此錯誤的常見元兇:

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 中的佈局錯誤)的資訊,請檢視以下資源: