今天預計跟大家一起探討什麼是組件的狀態,以及如何操作組件的狀態,分為下列部分:
好的,那我們就開始吧!
事物的「狀態」,大致上是指「綜合所有可以表達目前目標狀況的資訊」。例如我要顯示「感冒」的狀態,可以由「頭痛」、「喉嚨痛」、「流鼻水」等症狀表達出來。
一個組件的狀態則由這個組件內部所含的資料所表示。比如說一個最簡單的開關,現在的狀態是「開」?還是「關」?那就是由這個開關內部紀錄的資料來決定。
class Switch {
bool isOpen = false; // 當前開關是否打開(否)
Switch();
// 切換開關
void toggleSwitch(){
isOpen = !isOpen;
}
}
var fancySwitch = Switch();
print(fancySwitch.isOpen); // false
fancySwitch.toggleSwitch();
print(fancySwitch.isOpen); // true
如上,我定義的switch class內部有fieldisOpen
可以紀錄開關是否開啟,又定義method toggleSwitch
來調整isOpen,切換開關是否開啟。
這時候我們就說isOpen
表達了這個開關的狀態,是這個switch
的state。
當然一個組件可以不只有一個state,大多數時候都是綜合多筆資料一起表達組件狀態。
class Switch {
final bool isOpen;
Switch(this.isOpen);
}
var switchClose = Switch(false); // 關閉的開關,外部給予開關組件'false'值
var switchOpen = Switch(true); // 開啟的開關,外部給予開關組件'true'值
可以看到這個例子的區別:「是開是關」的資料並不是內部建立並操控的,而全然是由外部提供的。Switch只負責依照外部提供的資料來建立對應的開關而已。StatelessWidget
是一個不需要自己紀錄內部狀態的組件,需要的資料只能靠外部提供。
class Title extends StatelessWidget {
final String text;
const Title({super.key, reqiured this.text});
@override
Widget build(BuildContext context) {
return Text(
text,
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.w900,
),
),
}
如上例,我建立一個Title的無狀態組件,這個title不需要變動內部的資料,只要把交給它的text以指定的樣式呈顯就可以了。此時text就是這個Title的prop。
這樣一個Title組件就可以重複使用,並依照外部交給他的prop去改變標題的內容,不必每次都把要顯示的標題寫死在組建裡面。
Title(text:'今日標題');
Title(text:'明日標題');
StatefulWidget
是有自己內部狀態的組件,可以建立自己的state也可以接受外部的prop,一個StatefulWidget
會分開定義「widget」和「widget所使用的狀態」。
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title; // 外部傳入的props
@override
State<MyHomePage> createState() => _MyHomePageState();
}
以widget來說,widget在創建時可以接受外部prop,並且在此透過createState
定義自己的state操作
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0; // state
void _incrementCounter() {
setState(() {
_counter++; // 更動自己的state
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title), // 使用widget取得的prop
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter', // state透過Text組件顯示在畫面上,一旦state更新會觸發Text組件的重製
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter, // 調用變更state的_incrementCounter函式
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
而在State class這邊,透過setState()
來改變內部的狀態,標註所有與「被改變的state」相關的組件為待更新,並在build()
觸發時重繪被標註的組件,達到變更畫面的效果。
而為什麼StatelessWidget
只要一個class,而StatefuleWidget要特別拆成兩個class來處理呢?
由於Flutter 的widget創建後內部的資料是不能更動(immutable)的,要更改畫面必須重新創建,因此採用「創建widget」之後「注入state」方法,由state來掌管可變的狀態。
此外將state與widget拆分,也可以確保每次widget重製時,state可以獨立於widget之外仍然保留狀態。
這在官方文件有所說明。
那麼,StatelessWidget
和StatefulWidget
的使用時機分別為何呢?這點也沒有一定的規則,只能依當下的情況由開發者自行設計考量。
比如我可以讓一個開關組件不自己紀錄開關狀態,把資料記在外部,讓其他使用Switch widget的組件告訴這個組件應該開還是關:
class Switch extends StatelessWidget {
final bool isOpen;
const Switch({super.key,
required this.isOpen,
});
@override
Widget build(BuildContext context) {
return Text(isOpen?'Open':'Close');
}
var openSwitch = Switch(true); // 需要顯示一個「開」狀態的開關
我也可以讓Switch自己紀錄自己的開關狀態,調用自己的方法來切換開關
class Switch extends StatefulWidget {
const Switch({super.key});
@override
State<Switch> createState() => _SwitchState();
}
class _SwitchState extends State<Switch> {
bool _isOpen = false;
void toggleSwitch() {
setState(() {
_isOpen = !_isOpen;
});
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: toggleSwitch, // 按下開關就會切換開/關
child: Text(_isOpen ? 'Open' : 'Close'));
}
}
不過使用StatefulWidget
時要特別小心,由於state變更時,內部所有的結構都會被重新繪製,需要被繪製的組成愈多,就愈耗效能。因此應該盡量只讓需要變動的組件採用StatefulWidget
,而可以單純依照外部資料顯示的組件採用StatelessWidget
,才能最有效地分配運算資源。
例如:「開關」會有狀態的切換的情境,可以使用StatefuleWidget
;而「文字」、「圖形」等組件不用有太大的變動,只要內容傳進去就可以顯示了,相較之下更適合使用StatelessWidget
不過這些都沒有一定的標準。最後決定要把一個Widget設計成Statefule或Stateless,除了考量效能,也取決於我們想把邏輯封裝在哪一個層級的組件中,讓專案架構更乾淨、資料流更好管理。
今天大致了解了state, prop的概念,也看到stateful和stateless的差別。接下來要在何時使用statefule和stateless widget就是開發者設計上的問題了。
明天會和大家一起看看flutter的三個基本元件: Image, Text, Button,並實作於專案中~