貳、Building user interfaces with Flutter

我們會用一個購物清單的實作,來帶大家認識元件在Flutter中的相互使用與堆疊。並且,在接下來的章節中,我們會將視野著重於程式構造,將不再細講單一特定元件的屬性與使用方式!

一、Changing widgets in response to input: 根據輸入更改元件狀態

為了應對更加複雜的使用者互動,我們會使用「State(狀態)」來區分元件,最顯而易見的,就是使用Stateless與Stateful 元件。值得注意的是,我們先前介紹元件時大多都是使用「無狀態」的方式。

Stateless(無狀態)元件特性

  • 不可變性(Immutability):一旦創建,其屬性和內容不能被更改。它的配置在創建後保持不變。

  • 無內部狀態(No Internal State):它不維護任何關於自身狀態的資訊。它的外觀和內容完全由其屬性和外部數據決定。

  • 輕量(Lightweight):由於它們是不可變的且不需維護內部狀態,StatelessWidget通常相較StatefulWidget輕量。

  • 適用情境:適用於渲染過程中不會更改的UI元件,如靜態文字、按鈕、圖片等。

Stateful(有狀態)元件特性

  • 可變性(Mutability):可以在渲染過程中修改其屬性,並且可以維護內部狀態。

  • 有內部狀態(Internal State):具有一個對應的State對象,該State對象負責管理和維護元件的內部狀態。

  • 適用情境:適用於渲染過程中需要根據用戶動作而有變化的UI元件,例如表單輸入、動畫、計數器等。

SetState特性

在StatefulWidgets中,會有setState()函數,當執行到該所屬函數時,會根據改變的狀態重新做UI的建構渲染

1
2
3
4
5
void _increment() {
setState(() { //使用setState,以告訴Flutter框架狀態已經改變,需要重新構建UI以反映變化
_counter++;
});
}

Stateful使用範例

我們拿一個使用ElevatedButton的計數器程式作為說明。

Counter類別: 是一個StatefulWidget。

_CounterState類別: 是Counter的內部狀態,在這個類別內有_increment()函式

_increment()函式: 裡面有setState()用於放置對變數_counter的變化行為

ElevatedButton: 裡面的onPressed,會呼叫一次_increment()函式

所以當程式執行時,會先建構初始化的計數器畫面,並隨著使用者點擊按鈕,呼叫_increment(),並由setState更新累計的次數,並顯示當前的次數在畫面上。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
const Counter({super.key});

@override
State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
int _counter = 0;

void _increment() {
setState(() { //我們使用setState,以告訴Flutter框架狀態已經改變,需要重新構建UI以反映變化
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: _increment,//重新呼叫一次setState(),更新_counter
child: const Text('Increment'),
),
const SizedBox(width: 16),
Text('Count: $_counter'),
],
);
}
}

void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Center(
child: Counter(),
),
),
),
);
}

Stateful使用範例

二、Bringing it all together

我們回到購物車的範例,我們先設計出購物車項目的規劃,並且目前用inCart變數手動設為”false”,”true”來建構

  • Product類別:這是一個用來表示產品的簡單類別,它具有一個name屬性,表示產品的名稱。

  • CartChangedCallback:用於通知當前產品的購物車狀態是否發生變化。

  • ShoppingListItem類別:這是一個自定義的StatelessWidget,用於顯示購物清單中的每個項目。

ShoppingListItem使用範例(Stateless,無狀態模式)

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import 'package:flutter/material.dart';

class Product {
const Product({required this.name});//產品名稱

final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: ObjectKey(product));

final Product product;
final bool inCart; //表示產品是否已經添加到購物車
final CartChangedCallback onCartChanged;//當用戶點擊清單項目時,更新購物車狀態。

Color _getColor(BuildContext context) {//設定購物車內的項目顏色

return inCart //購物車回傳值
? Colors.black54
: Theme.of(context).primaryColor;
}

TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;

return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}

@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}

void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: ShoppingListItem(
product: const Product(name: 'Chips'),
inCart: true,//為false時是藍色、為true時是劃掉且顏色為灰色
onCartChanged: (product, inCart) {},
),
),
),
),
);
}

ShoppingListItem使用範例(Stateless,無狀態模式)

  • 輸出結果

inCart true的情況

inCart false的情況

_handleCartChanged函數,進行互動更新

我們現在新增一個_handleCartChanged函數,使用setState()來控制每次的點擊互動更新,我們讓按鈕被點擊時固定呼叫此函數去更新inCart的Boolean值,點擊一次會使其「移除」、再次進行點擊則會「加回」

1
2
3
4
5
6
7
8
9
void _handleCartChanged(Product product, bool inCart) {
setState(() {
if (!inCart) {
_shoppingCart.add(product);
} else {
_shoppingCart.remove(product);
}
});
}

_handleCartChanged函數

ShoppingListItem使用範例(Stateful,有狀態模式)

我們將剛剛Stateless的範例轉為使用Stateful來達成使用者動態更新購物車狀態,由剛剛的程式,我們可以新增_handleCartChanged函數處理購物車狀態,並設定三項購物車內容做demo

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import 'package:flutter/material.dart';

class Product {
const Product({required this.name});

final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: ObjectKey(product));

final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;

Color _getColor(BuildContext context) {

return inCart //
? Colors.black54
: Theme.of(context).primaryColor;
}

TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;

return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}

@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(
product.name,
style: _getTextStyle(context),
),
);
}
}

class ShoppingList extends StatefulWidget {
const ShoppingList({required this.products, super.key});

final List<Product> products;

@override
State<ShoppingList> createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
final _shoppingCart = <Product>{};

void _handleCartChanged(Product product, bool inCart) { //處理購物車狀態
setState(() {
if (!inCart) {
_shoppingCart.add(product);
} else {
_shoppingCart.remove(product);
}
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shopping List'),
),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: widget.products.map((product) {
return ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}

void main() {
runApp(const MaterialApp(
title: 'Shopping App',
home: ShoppingList(
products: [
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
),
));
}

ShoppingListItem使用範例(Stateful,有狀態模式)

  • 輸出結果