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 %}