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

理解 Flutter 中的佈局

#

Flutter 佈局機制的核心是 widget。在 Flutter 中,幾乎所有東西都是 widget——甚至佈局模型也是 widget。您在 Flutter 應用程式中看到的影像、圖示和文字都是 widget。您看不到的東西也是 widget,例如排列、約束和對齊可見 widget 的行、列和網格。

您透過組合 widget 來構建更復雜的 widget 來建立佈局。例如,下圖顯示了 3 個圖示,每個圖示下方都有一個標籤,以及相應的 widget 樹。

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

在此示例中,有一行 3 列,每列都包含一個圖示和一個標籤。所有佈局,無論多麼複雜,都是透過組合這些佈局 widget 建立的。

約束

#

理解 Flutter 中的約束是理解 Flutter 中佈局如何工作的重要組成部分。

佈局,從廣義上講,指的是 widget 的大小及其在螢幕上的位置。任何給定 widget 的大小和位置都受其父級約束;它不能擁有任何它想要的大小,它也不能決定它自己在螢幕上的位置。相反,大小和位置是由 widget 及其父級之間的對話決定的。

在最簡單的示例中,佈局對話如下所示:

  1. widget 從其父級接收其約束。
  2. 約束只是一組 4 個 double 值:最小和最大寬度,以及最小和最大高度。
  3. widget 在這些約束內確定其應有的大小,並將其寬度和高度傳回給父級。
  4. 父級檢視其想要的大小以及應如何對齊,並相應地設定 widget 的位置。對齊可以顯式設定,使用各種 widget,如 Center,以及 RowColumn 上的對齊屬性。

在 Flutter 中,這種佈局對話通常用簡化的短語表達:“約束向下傳遞。尺寸向上返回。父級設定位置。”

盒子型別

#

在 Flutter 中,widget 由其底層 RenderBox 物件渲染。這些物件決定如何處理傳遞給它們的約束。

通常,有三種盒子:

  • 那些嘗試儘可能大的。例如,CenterListView 使用的盒子。
  • 那些嘗試與其子級相同大小的。例如,TransformOpacity 使用的盒子。
  • 那些嘗試特定大小的。例如,ImageText 使用的盒子。

一些 widget,例如 Container,根據其建構函式引數的不同而型別不同。Container 建構函式預設情況下嘗試儘可能大,但如果您給它一個寬度,例如,它會嘗試遵循該寬度併成為該特定大小。

其他 widget,例如 RowColumn (彈性盒),根據它們獲得的約束而不同。在 理解約束文章 中閱讀有關彈性盒和約束的更多資訊。

佈局單個 widget

#

要在 Flutter 中佈局單個 widget,請將可見 widget (例如 TextImage) 用一個可以更改其在螢幕上位置的 widget (例如 Center widget) 包裝起來。

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 都具有以下任一屬性:

  • 如果它們接受單個子級,則為 child 屬性——例如,CenterContainerPadding
  • 如果它們接受 widget 列表,則為 children 屬性——例如,RowColumnListViewStack

容器

#

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。本頁上的第一個圖表同時使用了兩者。

這是使用 Row widget 的最基本示例。

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}
A screenshot of a row widget with three children
此圖顯示了一個帶有三個子級的行 widget。

RowColumn 的每個子級本身都可以是行和列,組合起來形成複雜的佈局。例如,您可以使用列為上述示例中的每個影像新增標籤。

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,每個子級都是一個列。

在行和列中對齊 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 屬性控制行或列如何對其子級進行對齊。對於行,主軸水平執行,交叉軸垂直執行。對於列,主軸垂直執行,交叉軸水平執行。

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,這些子級與 `MainAxisAlignment.spaceEvenly` 常量對齊。

列的工作方式與行相同。以下示例顯示了一個由 3 張影像組成的列,每張影像高 100 畫素。渲染盒的高度 (在此情況下,是整個螢幕) 大於 300 畫素,因此將主軸對齊設定為 spaceEvenly 會在每個影像之間、上方和下方均勻地劃分自由垂直空間。

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

MainAxisAlignmentCrossAxisAlignment 列舉提供了各種常量來控制對齊。

Flutter 包含其他可用於對齊的 widget,特別是 Align widget。

在行和列中調整 widget 大小

#

當佈局太大而無法適應裝置時,沿受影響的邊緣會出現黃色和黑色條紋圖案。在此示例中,視口寬 400 畫素,每個子級寬 150 畫素。

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

可以使用 Expanded widget 調整 widget 大小以適應行或列。為了修復上一個示例中影像行太寬而超出其渲染盒的問題,請將每個影像用一個 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,這些子級用 `Expanded` widget 包裝。

Expanded widget 還可以指定 widget 相對於其兄弟 widget 應占用多少空間。例如,您可能希望一個 widget 佔用其兄弟 widget 兩倍的空間。為此,請使用 Expanded widget 的 flex 屬性,這是一個整數,用於確定 widget 的 flex 因子。預設的 flex 因子為 1。以下程式碼將中間影像的 flex 因子設定為 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,這些子級用 `Expanded` widget 包裝。中心子級的 `flex` 屬性設定為 2。

DevTools 和除錯佈局

#

在某些情況下,盒子的約束是無界的,或者說是無限的。這意味著最大寬度或最大高度設定為 double.infinity。當給定無界約束時,嘗試儘可能大的盒子將無法正常工作,並且在除錯模式下會丟擲異常。

渲染盒最終出現無界約束最常見的情況是在彈性盒 (RowColumn) 內,以及在可滾動區域 (例如 ListView 和其他 ScrollView 子類) 內。例如,ListView 嘗試在其交叉方向上擴充套件以適應可用空間 (也許它是一個垂直滾動的塊,並嘗試與其父級一樣寬)。如果您將一個垂直滾動的 ListView 巢狀在一個水平滾動的 ListView 內,則內部列表嘗試儘可能寬,這將是無限寬,因為外部列表在該方向上是可滾動的。

也許您在構建 Flutter 應用程式時會遇到的最常見錯誤是由於不正確地使用佈局 widget 造成的,這被稱為“無界約束”錯誤。

如果您第一次開始構建 Flutter 應用程式時,只有一種型別錯誤需要準備好面對,那就是這種錯誤。

在新標籤頁中觀看 YouTube 影片:“Decoding Flutter: Unbounded height and width”

可滾動 widget

#

Flutter 具有許多內建的自動滾動 widget,還提供了各種可以自定義以建立特定滾動行為的 widget。在本頁中,您將看到如何使用最常見的 widget 使任何頁面可滾動,以及一個用於建立可滾動列表的 widget。

ListView

#

ListView 是一個類似列的 widget,當其內容長於其渲染盒時,它會自動提供滾動。使用 ListView 的最基本方法與使用 ColumnRow 非常相似。與列或行不同,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 建構函式。構建器建構函式只構建當前螢幕上可見的子級。

在以下示例中,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 用於建立移動、平板電腦、桌面和 Web 應用程式,您可能需要根據螢幕尺寸或輸入裝置等因素調整應用程式以使其行為不同。這被稱為使應用程式具有“自適應性”和“響應性”。

在建立自適應佈局中最有用的 widget 之一是 LayoutBuilder widget。LayoutBuilder 是 Flutter 中使用“構建器”模式的眾多 widget 之一。

構建器模式

#

在 Flutter 中,您會發現一些 widget 的名稱或建構函式中包含“builder”一詞。以下列表並不詳盡:

這些不同的“構建器”對於解決不同的問題很有用。例如,ListView.builder 建構函式主要用於懶惰地渲染列表中的專案,而 Builder widget 對於在深度 widget 程式碼中訪問 BuildContext 很有用。

儘管它們的使用場景不同,但這些構建器的工作方式是統一的。構建器 widget 和構建器建構函式都具有名為“builder”(或類似名稱,例如 ListView.builder 中的 itemBuilder)的引數,並且 builder 引數始終接受回撥。此回撥是一個構建器函式。構建器函式是向父 widget 傳遞資料的回撥,父 widget 使用這些引數來構建並返回子 widget。構建器函式總是至少傳入一個引數——構建上下文——並且通常至少傳入另一個引數。

例如,LayoutBuilder widget 用於根據視口的大小建立響應式佈局。構建器回撥主體會傳遞其從父級接收到的 BoxConstraints,以及 widget 的“BuildContext”。透過這些約束,您可以根據可用空間返回不同的 widget。

在新標籤頁中觀看 YouTube 影片:“LayoutBuilder (Flutter Widget of the Week)”

在以下示例中,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),
          ],
        ),
      );
    },
  );
}

此示例程式碼使用傳入構建器的索引從專案列表中獲取正確的待辦事項,然後將在構建器中返回的 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。

反饋

#

由於本網站的此部分正在不斷發展,我們歡迎您的反饋