跳至主要內容

版面配置

由於 Flutter 是一個 UI 工具包,您將花費大量時間使用 Flutter Widget 建立版面配置。在本節中,您將學習如何使用一些最常見的版面配置 Widget 來建構版面配置。您將使用 Flutter DevTools (也稱為 Dart DevTools) 來了解 Flutter 如何建立您的版面配置。最後,您將遇到並除錯 Flutter 最常見的版面配置錯誤之一,即可怕的「無界限約束」錯誤。

理解 Flutter 中的版面配置

#

Flutter 版面配置機制的中心是 Widget。在 Flutter 中,幾乎所有東西都是 Widget — 即使是版面配置模型也是 Widget。您在 Flutter 應用程式中看到的圖片、圖示和文字都是 Widget。您看不到的東西也是 Widget,例如排列、約束和對齊可見 Widget 的 Row、Column 和網格。

您可以透過組合 Widget 來建立更複雜的 Widget,進而建立版面配置。例如,下圖顯示了 3 個圖示,每個圖示下方都有一個標籤,以及對應的 Widget 樹狀結構

A diagram that shows widget composition with a series of lines and nodes.

在此範例中,有一個 3 個 Column 的 Row,每個 Column 都包含一個圖示和一個標籤。所有版面配置,無論多麼複雜,都是透過組合這些版面配置 Widget 來建立的。

約束條件

#

了解 Flutter 中的約束條件是了解 Flutter 中版面配置運作方式的重要一環。

從廣義上講,版面配置是指 Widget 的大小及其在螢幕上的位置。任何給定 Widget 的大小和位置都受其父 Widget 的約束;它不能擁有它想要的任何大小,並且它也不能決定自己在螢幕上的位置。相反地,大小和位置是由 Widget 和其父 Widget 之間的「對話」決定的。

在最簡單的範例中,版面配置「對話」看起來像這樣

  1. Widget 從其父 Widget 接收其約束條件。
  2. 約束條件只是一組 4 個 double:最小和最大寬度,以及最小和最大高度。
  3. Widget 決定其在這些約束條件內應有多大,並將其寬度和高度傳回給父 Widget。
  4. 父 Widget 會查看它想要的大小以及應如何對齊,並相應地設定 Widget 的位置。可以使用各種 Widget (例如 Center) 和 RowColumn 上的對齊屬性來明確設定對齊方式。

在 Flutter 中,此版面配置「對話」通常用簡化的短語來表達:「約束條件向下傳遞。大小向上傳遞。父 Widget 設定位置。」

Box 類型

#

在 Flutter 中,Widget 是由其底層的 RenderBox 物件呈現的。這些物件決定如何處理傳遞給它們的約束條件。

一般來說,有三種 Box

  • 那些試圖盡可能大的 Box。例如,CenterListView 所使用的 Box。
  • 那些試圖與其子 Widget 大小相同的 Box。例如,TransformOpacity 所使用的 Box。
  • 那些試圖成為特定大小的 Box。例如,ImageText 所使用的 Box。

某些 Widget (例如 Container) 會根據其建構函式引數而異。Container 建構函式預設為嘗試盡可能大,但如果您為它提供寬度,例如,它會嘗試遵循該寬度並成為該特定大小。

其他 Widget (例如 RowColumn (彈性 Box)) 會根據它們被賦予的約束條件而異。在了解約束條件文章中閱讀更多關於彈性 Box 和約束條件的資訊。

配置單一 Widget

#

若要在 Flutter 中配置單一 Widget,請使用可以更改其在螢幕上的位置的 Widget (例如 Center Widget) 包裹可見的 Widget (例如 TextImage)。

dart
Widget build(BuildContext context) {
  return Center(
    child: BorderedImage(),
  );
}

下圖顯示一個未在左側對齊的 Widget,以及一個在右側居中的 Widget。

A screenshot of a centered widget and a screenshot of a widget that hasn't been centered.

所有版面配置 Widget 都具有以下其中一個

  • 如果它們採用單一子 Widget,則具有 child 屬性 — 例如,CenterContainerPadding
  • 如果它們採用 Widget 清單,則具有 children 屬性 — 例如,RowColumnListViewStack

Container

#

Container 是一個方便的 Widget,由多個負責版面配置、繪圖、定位和調整大小的 Widget 組成。在版面配置方面,它可用於為 Widget 新增邊距和間距。這裡也可以使用 Padding Widget 來達到相同的效果。以下範例使用 Container

dart
Widget build(BuildContext context) {
  return Container(
    padding: EdgeInsets.all(16.0),
    child: BorderedImage(),
  );
}

下圖顯示一個左側沒有間距的 Widget,以及一個右側有間距的 Widget。

A screenshot of a widget with padding and a screenshot of a widget without padding.

若要在 Flutter 中建立更複雜的版面配置,您可以組合許多 Widget。例如,您可以組合 ContainerCenter

dart
Widget build(BuildContext context) {
  return Center(
    Container(
      padding: EdgeInsets.all(16.0),
      child: BorderedImage(),
    ),
  );
}

垂直或水平配置多個 Widget

#

最常見的版面配置模式之一是垂直或水平排列 Widget。您可以使用 Row Widget 水平排列 Widget,並使用 Column Widget 垂直排列 Widget。本頁面上的第一張圖使用了這兩種 Widget。

這是使用 Row Widget 最基本的範例。

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}

A screenshot of a row widget with three children
此圖顯示一個具有三個子 Widget 的 Row Widget。

RowColumn 的每個子 Widget 本身都可以是 Row 和 Column,組合起來構成一個複雜的版面配置。例如,您可以使用 Column 為上述範例中的每個圖片新增標籤。

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      Column(
        children: [
          BorderedImage(),
          Text('Dash 1'),
        ],
      ),
      Column(
        children: [
          BorderedImage(),
          Text('Dash 2'),
        ],
      ),
      Column(
        children: [
          BorderedImage(),
          Text('Dash 3'),
        ],
      ),
    ],
  );
}

A screenshot of a row of three widgets, each of which has a label underneath it.
此圖顯示一個具有三個子 Widget 的 Row Widget,每個子 Widget 都是一個 Column。

在 Row 和 Column 中對齊 Widget

#

在以下範例中,每個 Widget 的寬度為 200 像素,而視窗的寬度為 700 像素。因此,Widget 會從左到右依序對齊,而所有額外的空間都位於右側。

A diagram that shows three widgets laid out in a row. Each child widget is labeled as 200px wide, and the blank space on the right is labeled as 100px wide.

您可以使用 mainAxisAlignmentcrossAxisAlignment 屬性來控制 Row 或 Column 如何對齊其子 Widget。對於 Row,主軸水平延伸,而交叉軸垂直延伸。對於 Column,主軸垂直延伸,而交叉軸水平延伸。

A diagram that shows the direction of the main axis and cross axis in both rows and columns

將主軸對齊方式設定為 spaceEvenly 會在每個圖片之間、之前和之後均勻分割可用水平空間。

dart
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}

A screenshot of three widgets, spaced evenly from each other.
此圖顯示一個具有三個子 Widget 的 Row Widget,它們與 MainAxisAlignment.spaceEvenly 常數對齊。

Column 的運作方式與 Row 相同。以下範例顯示一個包含 3 個圖片的 Column,每個圖片的高度為 100 像素。渲染 Box (在本例中,為整個螢幕) 的高度大於 300 像素,因此將主軸對齊方式設定為 spaceEvenly 會在每個圖片之間、上方和下方均勻分割可用垂直空間。

A screenshot of a three widgets laid out vertically, using a column widget.

MainAxisAlignmentCrossAxisAlignment 列舉提供各種常數來控制對齊方式。

Flutter 包含其他可用於對齊的 Widget,尤其是 Align Widget。

在 Row 和 Column 中調整 Widget 大小

#

當版面配置太大而無法容納裝置時,受影響的邊緣會出現黃色和黑色條紋圖案。在此範例中,視窗的寬度為 400 像素,而每個子 Widget 的寬度為 150 像素。

A screenshot of a row of widgets that are wider than their viewport.

可以使用 Expanded Widget 來調整 Widget 的大小,使其符合 Row 或 Column。若要修正先前圖片的 Row 對其渲染 Box 來說太寬的範例,請使用 Expanded Widget 包裹每個圖片。

dart
Widget build(BuildContext context) {
  return const Row(
    children: [
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
    ],
  );
}

A screenshot of three widgets, which take up exactly the amount of space available on the main axis. All three widgets are equal width.
此圖顯示一個具有三個子 Widget 的 Row Widget,這些子 Widget 都使用 Expanded Widget 包裹。

Expanded Widget 也可以決定 Widget 相對於其同級 Widget 應佔用多少空間。例如,您可能希望一個 Widget 佔用其同級 Widget 兩倍的空間。為此,請使用 Expanded Widget 的 flex 屬性,這是一個整數,用於決定 Widget 的彈性係數。預設的彈性係數為 1。以下程式碼將中間圖片的彈性係數設定為 2

dart
Widget build(BuildContext context) {
  return const Row(
    children: [
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        flex: 2,
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
    ],
  );
}

A screenshot of three widgets, which take up exactly the amount of space available on the main axis. The widget in the center is twice as wide as the widgets on the left and right.
此圖顯示一個具有三個子 Widget 的 Row Widget,這些子 Widget 都使用 Expanded Widget 包裹。中間子 Widget 的 flex 屬性設定為 2。

DevTools 與除錯版面配置

#

在某些情況下,Box 的約束條件是無界的或無限的。這表示最大寬度或最大高度設定為 double.infinity。當被賦予無界限的約束條件時,試圖盡可能大的 Box 將無法正常運作,並且在除錯模式下會擲回例外狀況。

渲染 Box 最終獲得無界限約束條件的最常見情況是位於彈性 Box (RowColumn) 內,以及可捲動區域內 (例如 ListView 和其他 ScrollView 子類別)。例如,ListView 會嘗試擴展以符合其交叉方向的可用空間 (它可能是一個垂直捲動區塊,並嘗試與其父 Widget 一樣寬)。如果您在水平捲動的 ListView 內巢狀一個垂直捲動的 ListView,則內部清單會嘗試盡可能寬,這是無限寬,因為外部清單在該方向上是可捲動的。

您在建構 Flutter 應用程式時可能遇到的最常見錯誤是由於錯誤地使用版面配置 Widget 所導致,並且稱為「無界限約束條件」錯誤。

如果您在剛開始建構 Flutter 應用程式時只應準備好應對一種錯誤類型,那就是這個錯誤。


解碼 Flutter:無界限高度和寬度

可捲動 Widget

#

Flutter 有許多內建的 Widget 會自動捲動,並且還提供各種 Widget,您可以自訂這些 Widget 來建立特定的捲動行為。在本頁面上,您將了解如何使用最常見的 Widget 來使任何頁面可捲動,以及用於建立可捲動清單的 Widget。

ListView

#

ListView 是一個類似欄的 widget,當其內容長度超出其渲染框時,會自動提供捲動功能。使用 ListView 最基本的方式與使用 ColumnRow 非常相似。與 column 或 row 不同的是,ListView 要求其子項佔據交叉軸上的所有可用空間,如下例所示。

dart
Widget build(BuildContext context) {
  return ListView(
    children: const [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}

A screenshot of three widgets laid out vertically. They have expanded to take up all available space on the cross axis.
此圖顯示一個具有三個子項的 ListView widget。

當您有未知或非常龐大(或無限)數量的列表項目時,通常會使用 ListView。在這種情況下,最好使用 ListView.builder 建構函式。 builder 建構函式只會建構目前在螢幕上可見的子項。

在以下範例中,ListView 正在顯示一個待辦事項列表。待辦事項是從儲存庫中獲取的,因此待辦事項的數量是未知的。

dart
final List<ToDo> items = Repository.fetchTodos();

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, idx) {
      var item = items[idx];
      return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(item.description),
            Text(item.isComplete),
          ],
        ),
      );
    },
  );
}

A screenshot of several widgets laid out vertically. They have expanded to take up all available space on the cross axis.
此圖顯示 ListView.builder 建構函式,用於顯示未知數量的子項。

自適應版面配置

#

由於 Flutter 用於建立行動、平板電腦、桌面網頁應用程式,您可能需要調整您的應用程式,使其根據螢幕大小或輸入裝置等因素而有不同的行為。這被稱為使應用程式具有適應性響應性

在製作適應性版面配置時,最有用的 widget 之一是 LayoutBuilder widget。LayoutBuilder 是 Flutter 中許多使用「builder」模式的 widget 之一。

建構器模式

#

在 Flutter 中,您會發現一些在其名稱或建構函式中使用「builder」一詞的 widget。以下列表並不詳盡

這些不同的「builder」對於解決不同的問題非常有用。例如,ListView.builder 建構函式主要用於延遲渲染列表中的項目,而 Builder widget 則有助於在深層 widget 程式碼中存取 BuildContext

儘管它們的使用案例不同,但這些 builder 的運作方式是統一的。Builder widget 和 builder 建構函式都具有名為 'builder' 的引數(或類似的引數,例如 ListView.builder 中的 itemBuilder),而 builder 引數始終接受回呼。此回呼是一個建構函式。建構函式是將資料傳遞給父 widget 的回呼,而父 widget 使用這些引數來建構並傳回子 widget。建構函式始終至少傳入一個引數,即建構上下文,並且通常至少傳入另一個引數。

例如,LayoutBuilder widget 用於根據視窗的大小建立響應式版面配置。 builder 回呼主體會傳入它從其父級接收到的 BoxConstraints,以及 widget 的 'BuildContext'。使用這些約束,您可以根據可用空間傳回不同的 widget。


LayoutBuilder(Flutter 每週 Widget)

在以下範例中,LayoutBuilder 傳回的 widget 會根據視窗是否小於或等於 600 像素,或是大於 600 像素而改變。

dart
Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
      if (constraints.maxWidth <= 600) {
        return _MobileLayout();
      } else {
        return _DesktopLayout();
      }
    },
  );
}

Two screenshots, in which one shows a narrow layout and the other shows a wide layout.
此圖顯示一個窄版面配置(垂直排列其子項)和一個寬版面配置(以網格排列其子項)。

同時,ListView.builder 建構函式上的 itemBuilder 回呼會傳入建構上下文和一個 int。此回呼會針對列表中的每個項目呼叫一次,而 int 引數代表列表項目的索引。當 Flutter 建構 UI 時,第一次呼叫 itemBuilder 回呼時,傳遞給該函數的 int 是 0,第二次是 1,依此類推。

這允許您根據索引提供特定的配置。回想一下上面使用 ListView.builder 建構函式的範例

dart
final List<ToDo> items = Repository.fetchTodos();

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, idx) {
      var item = items[idx];
      return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(item.description),
            Text(item.isComplete),
          ],
        ),
      );
    },
  );
}

此範例程式碼使用傳遞到 builder 中的索引,從項目列表中抓取正確的待辦事項,然後在從 builder 傳回的 widget 中顯示該待辦事項的資料。

為了說明這一點,以下範例會變更每隔一個列表項目的背景顏色。

dart
final List<ToDo> items = Repository.fetchTodos();

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, idx) {
      var item = items[idx];
      return Container(
        color: idx % 2 == 0 ? Colors.lightBlue : Colors.transparent,
        padding: const EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(item.description),
            Text(item.isComplete),
          ],
        ),
      );
    },
  );
}

This figure shows a `ListView`, in which its children have alternating background colors. The background colors were determined programmatically based on the index of the child within the `ListView`.
此圖顯示一個 ListView,其中其子項具有交替的背景顏色。背景顏色是根據子項在 ListView 中的索引以程式方式決定的。

其他資源

#

API 參考

#

以下資源說明個別 API。

意見回饋

#

由於網站的此部分正在發展中,我們歡迎您提供回饋