版面配置
由於 Flutter 是一套 UI 工具包,你會花很多時間使用 Flutter 元件 (Widgets) 來建立版面配置。在本節中,你將學習如何使用一些最常見的版面配置元件 (Layout widgets) 來建立版面。你也會使用 Flutter DevTools(也稱為 Dart DevTools)來了解 Flutter 如何建立你的版面。最後,你將遇到並除錯 Flutter 最常見的版面錯誤之一——令人頭痛的「無界限限制(unbounded constraints)」錯誤。
理解 Flutter 的版面配置
#Flutter 版面配置機制的核心是元件 (Widgets)。在 Flutter 中,幾乎所有東西都是元件——即使是版面配置模型也是元件。你在 Flutter 應用程式中看到的圖片、圖示和文字,都是元件。你看不到的東西也是元件,例如排列、限制和對齊可見元件 (Widget) 的 rows(行)、columns(列)和 grids(網格)。
你可以透過組合元件來建立更複雜的元件,進而完成版面配置。例如,下圖顯示了三個圖示,每個圖示下方都有一個標籤,以及對應的元件樹:

在這個例子中,有一行包含三個列,每個列中都有一個圖示和一個標籤。所有的版面配置,不論多麼複雜,都是透過這些版面配置元件 (Layout widgets) 組合而成。
限制(Constraints)
#理解 Flutter 中的限制(constraints)是了解 Flutter 版面配置運作方式的重要部分。
一般來說,版面配置指的是元件的大小以及它們在螢幕上的位置。任何元件的大小和位置都會受到其父元件的限制;它不能隨意設定任何大小,也不能自己決定在螢幕上的位置。相反地,大小和位置是透過元件與其父元件之間的對話來決定的。
在最簡單的例子中,這個版面配置對話如下:
- 元件從其父元件接收限制(constraints)。
- 限制其實就是四個 double 數值的集合:最小和最大寬度,以及最小和最大高度。
- 元件根據這些限制決定自己應該有多大,然後將自己的寬度和高度回傳給父元件。
- 父元件根據它想要的大小以及應該如何對齊,設定元件的位置。對齊可以明確設定,例如使用各種元件如
Center,以及Row和Column上的對齊屬性。
在 Flutter 中,這個版面配置對話經常以簡化的說法表達為:「限制往下傳遞,大小往上回報,父元件設定位置。」
Box 類型
#在 Flutter 中,元件是由其底層的 RenderBox 物件來繪製。這些物件決定了它們如何處理所接收到的限制(constraints)。
一般來說,有三種 box(方塊)類型:
- 嘗試盡可能大。例如,
Center和ListView所使用的 box。 - 嘗試與其子元件一樣大。例如,
Transform和Opacity所使用的 box。 - 嘗試成為特定大小。例如,
Image和Text所使用的 box。
有些元件,例如 Container,會根據建構子的參數在不同 box 類型之間切換。Container 建構子預設會嘗試盡可能大,但如果你給它一個寬度,它就會嘗試遵守這個設定,變成特定大小。
其他元件,例如 Row 和 Column (flex boxes),則會根據所給的限制(constraints)而有所不同。你可以在 Understanding Constraints article 進一步閱讀有關 flex box 和限制的內容。
配置單一元件
#要在 Flutter 中配置單一元件,可以將一個可見元件 (Widget),如 Text 或 Image,包裹在一個可以改變其在螢幕上位置的元件中,例如 Center 元件。
Widget build(BuildContext context) {
return Center(
child: BorderedImage(),
);
}下圖顯示了左側一個未對齊的元件(Widget),以及右側一個已置中的元件(Widget)。

所有版面配置元件 (Layout widgets) 都具有以下其中一種屬性:
- 如果僅接受單一子元件,則具有
child屬性——例如Center、Container或Padding。 - 如果接受多個元件的清單,則具有
children屬性——例如Row、Column、ListView或Stack。
Container
#Container 是一個便利元件(Widget),它由多個負責版面配置、繪製(Painting)、定位與尺寸調整的元件所組成。在版面配置方面,它可用來為元件(Widget)添加內距(padding)與邊距(margin)。這裡同樣也可以使用 Padding 元件來達到相同效果。以下範例使用了 Container。
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
child: BorderedImage(),
);
}下圖左側顯示沒有左側內距(padding)的元件(Widget), 右側則顯示有內距的元件。

若要在 Flutter 中建立更複雜的版面配置, 你可以組合多個元件(Widgets)。 例如,你可以結合 Container 和 Center:
Widget build(BuildContext context) {
return Center(
Container(
padding: EdgeInsets.all(16.0),
child: BorderedImage(),
),
);
}垂直或水平排列多個元件(Widgets)
#其中一個最常見的版面配置模式,就是將元件(Widgets)垂直或水平排列。
你可以使用 Row 元件來水平排列元件,
也可以使用 Column 元件來垂直排列元件。
本頁的第一張圖同時使用了這兩種元件。
以下是使用 Row 元件的最基本範例。Widget build(BuildContext context) {
return Row(
children: [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}
每個 Row 或 Column 的子項目(child)本身也可以是 rows 或 columns,彼此組合以建立複雜的版面配置。 例如,你可以在上述範例中的每個圖片下方,透過 columns 加上標籤。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'),
],
),
],
);
}
在 Row 和 Column 中對齊元件 (Widgets)
#在以下範例中, 每個元件 (Widget) 的寬度為 200 像素, 而檢視區(viewport)的寬度為 700 像素。 因此,這些元件會依序向左對齊, 多餘的空間則集中在右側。

你可以透過 mainAxisAlignment 和 crossAxisAlignment 屬性來控制 Row 或 Column 如何對齊其子元件 (children)。 對於 Row 來說,主軸(main axis)是水平方向, 而交叉軸(cross axis)是垂直方向;對於 Column, 主軸則是垂直方向,交叉軸則是水平方向。

將主軸對齊方式設為 spaceEvenly 會將多餘的水平空間平均分配在每個圖片之間、前面以及後面。Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}
欄(Column)與列(Row)運作方式相同。 以下範例展示了一個由 3 張圖片組成的欄, 每張圖片高度為 100 像素。該渲染框(在此例中為整個螢幕)的 高度超過 300 像素,因此將主軸對齊(main axis alignment)設為 spaceEvenly 會將多餘的垂直空間平均分配在每張圖片之間、上方與下方。

MainAxisAlignment 和 CrossAxisAlignment 這兩個 enum 提供多種常數,可用來控制對齊方式。
Flutter 也包含其他可用於對齊的元件(Widgets),其中較常用的是 Align 元件。
在 Row 與 Column 中調整元件尺寸
#當版面配置超出裝置可顯示範圍時, 受影響的邊緣會出現黃黑相間的條紋圖案。 在此範例中,檢視區(viewport)寬度為 400 像素, 每個子元件寬度為 150 像素。

可以使用 Expanded 元件,將元件尺寸調整至適合於 Row 或 Column 之中。若要修正前述圖片列過寬 導致超出渲染框的問題,請將每張圖片包裹在 Expanded 元件中。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),
),
],
);
}
Expanded 元件(Widget)也可以決定某個元件(Widget)相對於其兄弟元件應該佔用多少空間。例如,你可能希望某個元件佔用的空間是其兄弟元件的兩倍。這時,可以使用 Expanded 元件的 flex 屬性,這是一個整數,用來決定該元件的彈性係數(flex factor)。預設的彈性係數為 1。以下程式碼將中間圖片的彈性係數設為 2: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),
),
],
);
}
DevTools 與版面配置除錯
#在某些情況下,一個 box 的限制(constraint)是無界(unbounded)或無限(infinite)的。這代表最大寬度或最大高度被設為 double.infinity。當一個 box 嘗試盡可能變大時,如果給予它無界的限制,在 debug 模式下會丟出例外,因為這樣的 box 無法正常運作。
最常見的情況是,當一個 render box 處於 flex box(如 Row 或 Column)內,或是在可滾動區域(例如 ListView 及其他 ScrollView 子類別)中時,會遇到無界限制。例如,ListView 會嘗試在交錯方向(cross-direction)上展開以符合可用空間(也許它是一個垂直滾動的區塊,並嘗試與其父元件一樣寬)。如果你將一個垂直滾動的 ListView 巢狀放在一個水平滾動的 ListView 內,內層的列表會嘗試變得盡可能寬,而這個寬度是無限的,因為外層在該方向上是可滾動的。
你在開發 Flutter 應用程式時,最常遇到的錯誤之一,就是錯誤地使用版面配置元件(Layout widgets),這類錯誤被稱為「無界限制(unbounded constraints)」錯誤。
如果你剛開始開發 Flutter 應用程式,只需要準備面對一種型別的錯誤,那就是這個。
Watch on YouTube in a new tab: "Decoding Flutter: Unbounded height and width"
滾動元件 (Scrolling Widgets)
#Flutter 內建許多會自動滾動的元件(Widgets),也提供多種可自訂的元件,讓你建立特定的滾動行為。 本頁將介紹如何使用最常見的元件來讓任何頁面可滾動,以及建立可滾動列表的元件。
ListView
#ListView 是一個類似 column 的元件,當其內容超過自身 render box 長度時,會自動提供滾動功能。 最基本的 ListView 用法與 Column 或 Row 非常相似。 不同於 column 或 row,ListView 要求其子元件(children)在交錯軸(cross axis)上必須佔滿所有可用空間,如下方範例所示。Widget build(BuildContext context) {
return ListView(
children: const [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}
ListView 通常用於當你有未知數量、非常大量(或無限)的清單項目時。
在這種情況下,建議使用 ListView.builder 建構函式(constructor)。
builder 建構函式只會建立目前螢幕上可見的子元件(children)。
在以下範例中,ListView 正在顯示一個待辦事項(to-do)清單。
這些待辦事項是從資料儲存庫(repository)中取得,
因此待辦事項的數量是未知的。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),
],
),
);
},
);
}
自適應版面配置
#由於 Flutter 可用於建立行動裝置、平板、桌面以及網頁應用程式,因此你很可能需要根據螢幕大小或輸入裝置等因素,調整應用程式的行為。這種根據不同環境調整應用程式的作法,稱為讓應用程式具備「自適應」(adaptive)與「響應式」(responsive)特性。
在實現自適應版面配置時,其中一個最實用的元件(Widget)就是 LayoutBuilder 元件。LayoutBuilder 是 Flutter 中眾多採用「builder」模式的元件之一。
builder 模式
#在 Flutter 中,你會發現有許多元件的名稱或建構函式中包含「builder」這個詞。以下清單並非全部:
這些不同的「builder」元件適用於解決不同的問題。例如,ListView.builder 建構函式主要用於延遲渲染清單中的項目,而 Builder 元件則適合在深層元件程式碼中存取 BuildContext。
儘管用途不同,這些 builder 元件的運作方式是一致的。builder 元件與 builder 建構函式都會有名為 'builder' 的參數(或類似名稱,例如 ListView.builder 中的 itemBuilder),而這個 builder 參數總是接受一個回呼函式(callback)。這個回呼函式稱為 builder 函式。builder 函式會將資料傳遞給父元件,父元件則利用這些參數來建立並回傳子元件。builder 函式至少會傳入一個參數──build context──通常還會有其他參數。
舉例來說,LayoutBuilder 元件可用來根據檢視區(viewport)的大小建立響應式版面配置。builder 回呼函式的主體會從父元件接收到 BoxConstraints,以及元件的 'BuildContext'。有了這些限制條件,你就能根據可用空間回傳不同的元件。
Watch on YouTube in a new tab: "LayoutBuilder (Flutter Widget of the Week)"
在下方範例中,LayoutBuilder 所回傳的元件會根據檢視區寬度是否小於或等於 600 像素,或大於 600 像素而有所不同。Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth <= 600) {
return _MobileLayout();
} else {
return _DesktopLayout();
}
},
);
}
同時,ListView.builder 建構函式中的 itemBuilder 回呼函式會接收 build context 以及 int。
這個回呼函式會針對清單中的每一個項目各自被呼叫一次,
而 int 參數則代表該清單項目的索引。
每當 Flutter 建立 UI 時,第一次呼叫 itemBuilder 回呼時,
傳入的 int 會是 0,第二次則是 1,依此類推。
這讓你可以根據索引提供特定的設定。
請回想上方使用 ListView.builder 建構函式的範例:
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 的索引(index),從項目清單中取得正確的待辦事項(todo),然後將該待辦事項的資料顯示在 builder 回傳的元件(Widget)中。
為了說明這一點,下列範例會將每個清單項目的背景顏色交錯變化。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),
],
),
);
},
);
}
其他資源
#- 常用版面配置元件 (Layout widgets) 與概念
- 元件 (Widgets) 的尺寸與定位
- 可捲動元件 (Scrollable widgets)
- 自適應應用程式 (Adaptive Apps)
API 參考
#以下資源說明各個 API。
意見回饋
#由於本網站區塊仍在持續演進中,
我們歡迎您的意見回饋!