Flutter:由 TextField 來看 Widget 如何保留狀態

img

前言

在 Flutter 開發過程中,可以說大部分都環繞在 StatelessWidgetStatefulWidget 之間,無狀態的 StatelessWidget 主要是做一次性的建置(build),Flutter 在建構這個 Widget 時會去呼叫 build 方法一次。
img
最常使用的文件顯示 Widget - Text 便是 StatelessWidget,所以它在繪製完文字內容之後便不能再修改。
img
有狀態的 StatefulWidget 則是可以重複建置(build),它將建置動作交由 State 來處理,State 這個類別還提供一個 setState 方法,透過這個方法可以驅使 Flutter 再次呼叫 State 的 build 方法來重新建置 Widget,因此我們可以在 State 內宣告類別層級的變數來儲存資料,當build 方法重新建立 Widget 時,再將資料回填到對應的屬性,藉此達到狀態保留的功能。
img
最常使用的文件編輯 Widget - TextField 便是 StatefulWidget,我們每多輸入一個文字它會重新建置一次,但是他可以保留之前的內容(狀態)並將新輸入的文字累加進去。
img
剛說到 Text 無法重新建置,所以當呈現的文字內容要變更時,一般都是透過外層的 StatelessWidget 直接重新建立,當然如果需要保留它的狀態也必須透過外部暫存。

神奇的 TextField

我們再進一步思考 State 的 build 方法會重新建置 Widget,這意味著透過 build 方法所建立的 Widget 不論是 StatelessWidget 或是 StatefulWidget 都無法保留自己狀態,除非我們特別將狀態儲存起來。
所以我們從官網文件 Handle changes to a text field 可以看到,要保留 TextField 的狀態方法可以宣告一個變數來儲存,並在 TextField 的 onChanged 事件內將目前的內容儲存到變數內。

main.dart
1
2
3
4
5
6
7
8
9
10
class _MyHomePageState extends State<MyHomePage> {
String data = '';

@override
Widget build(BuildContext context) {
var input = TextField(
onChanged: onChanged: (text) => data = text,
);
...
}

或者透過 TextEditingController 來儲存。

main.dart
1
2
3
4
5
6
7
8
9
10
class _MyHomePageState extends State<MyHomePage> {
final myController = TextEditingController();

@override
Widget build(BuildContext context) {
var input = TextField(
controller: myController,
);
...
}

沒仔細想可能不會發現有些奇怪的地方,我們直接在專案預設範例內加入一個 TextField,而且不要幫它儲存任何狀態,主要程式碼如下:

main.dart
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
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('$_counter'),
TextField(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() {
_counter++;
}),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

因為 FloatingActionButton 在點擊時會呼叫 setState 方法,因此每次按它時就會執行 build 方法來重建 Widget,所以理論上 TextField 內容會被清空,接下來我們直接執行測試看看。
img
神奇的事情發生了,既使透過 build 重建 TextField,但是 TextField 的內容仍然被保留下來,看來 TextField 似乎是一個在規則外的特殊 Widget。

TextEditingController

TextEditingController 跟我們使用變數來儲存內容有什麼差別,從原始碼可以知道 TextEditingController 繼承自 ValueNotifier,而 TextEditingValue 除了儲存文字內容(text)之外還會多儲存其他狀態。
img
接著我們來從 TextField 的原始碼可以看到如下圖的關係:
img

  • State 內含 widget 屬性可以得知自己是由哪一個 StatelessWidget 所實作出來的,當然也可以藉此取得 StatelessWidget 的屬性。
  • _TextFieldState 也建立一個 TextEditingController 變數 _controller,以及一個 _effectiveController 屬性,這個屬性主要是回傳我們在建立 TextField 時給予的 controller,如果我們未給予值時則以 _controller 替代。
main.dart
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
class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin {
...
@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;
}
}
}
...
}