[Flutter] 排版應用介紹

作為一名主要開發平台為網頁的開發者,進入 Flutter 世界的開發體驗讓我發現元件結構是最需要適應的地方。在網頁開發中,我們可以透過 CSS 樣式來控制元素的排版,包括長寬、內外邊距,甚至彈性佈局 (flex) 或網格系統 (grid) 等。除非使用定位 (position),否則所有元素都處於同一層級。然而在 app 中,每個元素都被當作不同的「層」來處理,因此為了達到類似網頁的效果,必須使用不同的元件進行組合。

長寬和內外邊距的控制

在 Flutter 中,長寬和內外邊距的控制可以透過不同的元件來實現。以下是兩種常見的元件:

SizeBox

如果只需要設定元件的長寬,可以使用 SizeBox 元件。例如:

1
2
3
4
var comp = const SizedBox(
height: 36,
child: Text('我是文字')
);

若想讓元件自動擴展到 parent 的大小,可以使用 Expanded 或 Flexible 元件。

內外邊距

若想同時控制元件的內外邊距、長寬、背景色等等,可以使用 Container 元件。Container 還可以設定邊框、圓角等等,是相當常用的元件之一。例如:

1
2
3
4
5
6
7
8
var comp = Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.all(16.0),
color: Colors.grey.shade200,
height: 100,
width: 100,
child: const Text('我是文字'),
);

image-20230412222104812

元件排版

統一內縮

想要一個區塊內的元件都有同樣的內縮並且有背景色,那我們可以使用 Container 搭配 Column 這個元件來完成。

1
2
3
4
5
6
7
8
9
10
11
var comp = Container(
padding: const EdgeInsets.all(16),
color: Colors.grey.shade200,
child: Column(
children: [
const Text('文字1'),
const Padding(padding: EdgeInsets.only(top: 16.0)),
const Text('文字2'),
],
),
);

column

併排呈現

當有好幾個元件,想要再同一排中做呈現的時候,可以使用 Row 這個元件。

1
2
3
4
5
6
7
8
9
10
11
12
var comp = Container(
padding: const EdgeInsets.all(16),
color: Colors.grey.shade200,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Text('文字1'),
const Padding(padding: EdgeInsets.only(left: 16.0)),
const Text('文字2'),
],
),
);

row

進階使用

自動寬高度

如果希望併排的元件中,有一些是固定寬度,而剩下的元件則佔滿剩餘的全部寬度,可以使用 Expanded。在 Row 中使用 Expanded 元件時,會讓剩下的空間平均分配給 Expanded 元件,因此可以讓剩下的元件都自動調整寬度。

Column 中也可以使用 Expanded 元件來達成自動調整高度的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var comp = Container(
padding: const EdgeInsets.all(16),
color: Colors.grey.shade200,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Expanded(
child: Text('文字1'),
),
const Padding(padding: EdgeInsets.only(left: 16.0)),
const Text('文字2'),
],
),
);

expand-in-row

Flex

如果想要更精細地控制元件的寬度和高度,可以使用 Flexible 元件。設定 flex 屬性,讓元件按照比例分配空間。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var comp = Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// 只會佔用剩下的空間
Flexible(
child: Container(
color: Colors.amber,
width: 100,
),
),
Flexible(
// 佔用 10 等份
flex: 10,
child: Container(
color: Colors.teal,
),
),
// 固定大小
Container(
color: Colors.cyan,
width: 200,
),
],
);

看到底下結果是第一個元件並不會真正佔用到 100,反而會是第二和第三個元件所佔用的空間後剩下來的才會分配給他。

flex-in-row

有雷要注意

併排呈現

當使用 Row 的時候,底下的元件總寬度必須要在裝置的寬度之內,不然就會直接噴錯,在這邊是建議直接加上 Flexiable 讓元件平均分配,避免這個問題產生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var comp = Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
color: Colors.amber,
width: 200,
),
Container(
color: Colors.teal,
width: 200,
),
Container(
color: Colors.cyan,
width: 200,
),
],
);

not-enough-space-in-row

error-message-for-row

垂直呈現

在 Flutter 中,預設的排版方式是垂直往下,當元件超過裝置的高度時,也是一樣會噴錯,那這時候可以加上捲軸的元件 SingleChildScrollView,就可以解決高度不夠的問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var comp = SingleChildScrollView(
child: Column(
children: [
Container(
color: Colors.amber,
height: 500,
),
Container(
color: Colors.teal,
height: 200,
),
Container(
color: Colors.cyan,
height: 300,
),
],
),
);

over-height

結論

在 Flutter 的世界中,當你的排版比較複雜,要往下排又有元件要併排,然後還要控制內外邊距時,就會有很多階層的產生,這時候會建議適度的抽成元件,讓主要的 build 比較容易懂。

如果你跟我一樣主要是網頁的開發者,記得請拋棄你的既定印象和習慣,多多使用元件來搭配,才能排版出符合設計的樣式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(38),
color: const Color.fromRGBO(217, 217, 217, 0.5),
child: Column(
children: [
_LastNameInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_FirstNameInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_BirthdayInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_GenderInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_PhoneInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_EmailInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_PasswordInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_ConfirmedPasswordInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_ReceiveNewsInput(),
const Padding(padding: EdgeInsets.only(top: 12.0)),
_AgreePolicyInput(),
const Padding(padding: EdgeInsets.only(top: 40.0)),
_RegisterButton(),
],
),
),
);