跳至主要內容

常見的 Flutter 錯誤

簡介

#

本頁說明幾個常見的 Flutter 框架錯誤(包括版面配置錯誤),並提供如何解決這些錯誤的建議。這是一份不斷更新的文件,未來版本將會新增更多錯誤,歡迎您提供貢獻。歡迎開啟 Issue提交 Pull Request,讓本頁對您和 Flutter 社群更有幫助。

執行應用程式時出現純紅色或灰色畫面

#

通常稱為「紅色(或灰色)死亡畫面」,這有時是 Flutter 通知您發生錯誤的方式。

當應用程式在偵錯或設定檔模式下執行時,可能會出現紅色畫面。當應用程式在發布模式下執行時,可能會出現灰色畫面。

一般而言,這些錯誤會在發生未捕獲的例外狀況時(您可能需要另一個 try-catch 區塊),或發生某些渲染錯誤(例如溢位錯誤)時發生。

以下文章針對偵錯這類錯誤提供了一些實用見解

「A RenderFlex overflowed…」

#

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 有一個大小未受限的子 Widget 時,通常會發生此錯誤。例如,以下程式碼片段示範了一個常見的案例

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 Widget 沒有限制其子項的大小,Column Widget 也沒有。由於缺少來自其父項 Widget 的限制,第二個 Text Widget 嘗試與其需要顯示的所有字元一樣寬。然後,Text Widget 的自行決定寬度會被 Column 採用,這與其父項 Row Widget 可以提供的最大水平空間衝突。

如何修正?

好吧,您需要確保 Column 不會嘗試比它能容納的更寬。若要實現此目的,您需要限制其寬度。一種方法是將 Column 包裝在 Expanded Widget 中

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

另一種方法是將 Column 包裝在 Flexible Widget 中,並指定一個 flex 係數。事實上,Expanded Widget 等同於 flex 係數為 1.0 的 Flexible Widget,如其原始碼所示。若要進一步瞭解如何在 Flutter 版面配置中使用 Flex Widget,請查看這個關於 Flexible Widget 的 90 秒「本週 Widget」影片

更多資訊

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

「RenderBox was not laid out」

#

雖然這個錯誤很常見,但它通常是在渲染管道中較早發生的主要錯誤的副作用。

錯誤看起來像什麼?

錯誤顯示的訊息如下

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

您可能如何遇到此錯誤?

通常,問題與違反 Box 限制有關,需要透過提供更多關於您希望如何限制相關 Widget 的資訊來解決。您可以在瞭解限制頁面上深入瞭解 Flutter 中的限制如何運作。

RenderBox was not laid out 錯誤通常是由以下兩個其他錯誤之一引起的

  • 「Vertical viewport was given unbounded height」
  • 「An InputDecorator...cannot have an unbounded width」

「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(或其他種類的可捲動 Widget,例如 GridView)放置在 Column 內時,通常會發生此錯誤。ListView 會佔用所有可用的垂直空間,除非它受到其父項 Widget 的限制。但是,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 Widget 包裝它(如下列範例所示)。否則,請使用 SizedBox Widget 指定絕對高度,或使用 Flexible Widget 指定相對高度。

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」

#

錯誤訊息表示它也與 Box 限制有關,瞭解這些限制對於避免許多最常見的 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 Widget 限制文字欄位來修正此錯誤。以下範例示範了如何使用 Expanded Widget

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」

#

此錯誤與缺少預期的父項 Widget 有關。

錯誤看起來像什麼?

錯誤顯示的訊息如下

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 的 Widget 在 UI 中如何組合在一起通常很靈活,但一小部分 Widget 需要特定的父項 Widget。當無法在您的 Widget 樹狀結構中滿足此要求時,您可能會遇到此錯誤。

以下是不完整的 Widget 清單,這些 Widget 需要 Flutter 框架內特定的父項 Widget。歡迎您提交 PR(使用頁面右上角的說明文件圖示)以擴充此清單。

Widget預期的父項 Widget
FlexibleRowColumnFlex
Expanded(一個專門的 FlexibleRowColumnFlex
PositionedStack
TableCellTable

如何修正?

一旦您知道缺少哪個父項 Widget,修正方法應該就很明顯了。

「setState called during 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)

您可能如何遇到此錯誤?

一般而言,當在 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 連接到多個捲軸視圖

#

當螢幕上同時出現多個捲動 Widget(例如 ListView)時,可能會發生此錯誤。在網頁或桌面應用程式上,比在行動應用程式上更容易發生此錯誤,因為在行動裝置上很少會遇到這種情況。

如需更多資訊並瞭解如何修正,請查看以下關於 PrimaryScrollController 的影片


參考資料

#

若要深入瞭解如何偵錯錯誤,特別是 Flutter 中的版面配置錯誤,請查看以下資源