title: Flutter:由 TextField 來看 Widget 如何保留狀態
date: 2019-07-10
categories: Flutter
keywords:
在 Flutter 開發過程中,可以說大部分都環繞在 StatelessWidget 與 StatefulWidget 之間,無狀態的 StatelessWidget 主要是做一次性的建置(build
),Flutter 在建構這個 Widget 時會去呼叫 build 方法一次。
最常使用的文件顯示 Widget - Text 便是 StatelessWidget,所以它在繪製完文字內容之後便不能再修改。
有狀態的 StatefulWidget 則是可以重複建置(build
),它將建置動作交由 State 來處理,State 這個類別還提供一個 setState
方法,透過這個方法可以驅使 Flutter 再次呼叫 State 的 build
方法來重新建置 Widget,因此我們可以在 State 內宣告類別層級的變數來儲存資料,當build
方法重新建立 Widget 時,再將資料回填到對應的屬性,藉此達到狀態保留的功能。
最常使用的文件編輯 Widget - TextField 便是 StatefulWidget,我們每多輸入一個文字它會重新建置一次,但是他可以保留之前的內容(狀態)並將新輸入的文字累加進去。
剛說到 Text 無法重新建置,所以當呈現的文字內容要變更時,一般都是透過外層的 StatelessWidget 直接重新建立,當然如果需要保留它的狀態也必須透過外部暫存。
我們再進一步思考 State 的 build
方法會重新建置 Widget,這意味著透過 build
方法所建立的 Widget 不論是 StatelessWidget 或是 StatefulWidget 都無法保留自己狀態,除非我們特別將狀態儲存起來。
所以我們從官網文件 Handle changes to a text field 可以看到,要保留 TextField 的狀態方法可以宣告一個變數來儲存,並在 TextField 的 onChanged
事件內將目前的內容儲存到變數內。
{% codeblock main.dart lang:dart %}
class _MyHomePageState extends State
String data = ‘’;
@override
Widget build(BuildContext context) {
var input = TextField(
onChanged: onChanged: (text) => data = text,
);
…
}
{% endcodeblock %}
或者透過 TextEditingController 來儲存。
{% codeblock main.dart lang:dart %}
class _MyHomePageState extends State
final myController = TextEditingController();
@override
Widget build(BuildContext context) {
var input = TextField(
controller: myController,
);
…
}
{% endcodeblock %}
沒仔細想可能不會發現有些奇怪的地方,我們直接在專案預設範例內加入一個 TextField,而且不要幫它儲存任何狀態,主要程式碼如下:
{% codeblock main.dart lang:dart %}
class _MyHomePageState extends State
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children:
Text(‘$_counter’),
TextField(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() {
_counter++;
}),
tooltip: ‘Increment’,
child: Icon(Icons.add),
),
);
}
}
{% endcodeblock %}
因為 FloatingActionButton 在點擊時會呼叫 setState
方法,因此每次按它時就會執行 build
方法來重建 Widget,所以理論上 TextField 內容會被清空,接下來我們直接執行測試看看。
神奇的事情發生了,既使透過 build
重建 TextField,但是 TextField 的內容仍然被保留下來,看來 TextField 似乎是一個在規則外的特殊 Widget。
TextEditingController 跟我們使用變數來儲存內容有什麼差別,從原始碼可以知道 TextEditingController 繼承自 ValueNotifiertext
)之外還會多儲存其他狀態。
接著我們來從 TextField 的原始碼可以看到如下圖的關係:
widget
屬性可以得知自己是由哪一個 StatelessWidget 所實作出來的,當然也可以藉此取得 StatelessWidget 的屬性。 _controller
,以及一個 _effectiveController
屬性,這個屬性主要是回傳我們在建立 TextField 時給予的 controller
,如果我們未給予值時則以 _controller
替代。 {% codeblock main.dart lang:dart %}
class _TextFieldState extends State
…
@override
void initState() {
super.initState();
if (widget.controller == null)
_controller = TextEditingController();
}
@override
void didUpdateWidget(TextField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null)
_controller = TextEditingController.fromValue(oldWidget.controller.value);
else if (widget.controller != null && oldWidget.controller == null)
_controller = null;
final bool isEnabled = widget.enabled ?? widget.decoration?.enabled ?? true;
final bool wasEnabled = oldWidget.enabled ?? oldWidget.decoration?.enabled ?? true;
if (wasEnabled && !isEnabled) {
_effectiveFocusNode.unfocus();
}
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) {
if(_effectiveController.selection.isCollapsed) {
_showSelectionHandles = !widget.readOnly;
}
}
}
…
}
{% endcodeblock %}