Skip to main content

具狀態元件

瞭解 StatefulWidget 以及如何重新建置 Flutter UI。

瞭解元件何時需要具有狀態,以及如何使用 setState 觸發 UI 更新。

你將完成的事項

瞭解元件何時需要具有狀態
將 StatelessWidget 轉換為 StatefulWidget
使用 setState 觸發 UI 更新

Steps

1

簡介

到目前為止,你的應用程式顯示了一個格狀方格和一個輸入欄位,但方格尚未更新以反映使用者的猜測。當此應用程式完成時,下一個未填滿列中的每個方塊,應在每次提交使用者猜測後更新,具體方式為:

  • 顯示正確的字母。
  • 變更顏色以反映字母是否正確(綠色)、字母存在於單詞中但位置錯誤(黃色),或字母完全不在單詞中(灰色)。

為了處理這種動態行為,你需要將 GamePage 從 StatelessWidget 轉換為 StatefulWidget。

2

為何需要具狀態元件?

當元件 (Widget) 的外觀或資料需要在其生命週期內改變時,你需要使用 StatefulWidget 以及配套的 State 物件。雖然 StatefulWidget 本身仍然是不可變的(其屬性在建立後無法更改),但 State 物件是長期存在的,可以保存可變的資料,並且當資料發生變化時可以重新建置,從而使 UI 更新。

例如,以下的元件樹展示了一個簡單的應用程式範例,該應用程式使用具狀態元件,其計數器會在按下按鈕時遞增。

A diagram of a widget tree with a stateful widget and state object.

以下是基本的 StatefulWidget 結構(目前尚未執行任何操作):

dart
class ExampleWidget extends StatefulWidget {
  ExampleWidget({super.key});
​
  @override
  State<ExampleWidget> createState() => _ExampleWidgetState();
}
​
class _ExampleWidgetState extends State<ExampleWidget> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
3

將 GamePage 轉換為具狀態元件

要將 GamePage(或任何其他)元件從無狀態元件轉換為具狀態元件,請執行以下步驟:

  1. 將 GamePage 改為繼承 StatefulWidget 而非 StatelessWidget。
  2. 建立一個名為 _GamePageState 的新類別,繼承 State<GamePage>。這個新類別將保存可變的狀態和 build 方法。將 build 方法以及所有在元件上實例化的屬性從 GamePage 移至狀態物件。
  3. 在 GamePage 中實作 createState() 方法,該方法會回傳 _GamePageState 的實例。

修改後的程式碼應如下所示:

dart
class GamePage extends StatefulWidget {
  GamePage({super.key});
​
  @override
  State<GamePage> createState() => _GamePageState();
}
​
class _GamePageState extends State<GamePage> {
  final Game _game = Game();
​
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: [
          for (var guess in _game.guesses)
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                for (var letter in guess)
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 2.5, vertical: 2.5),
                    child: Tile(letter.char, letter.type),
                  )
              ],
            ),
          GuessInput(
            onSubmitGuess: (_) {
              // TODO, handle guess
              print(guess); // Temporary
            },
          ),
        ],
      ),
    );
  }
}
4

使用 setState 更新 UI

每當你修改 State 物件時,你必須呼叫 setState 來通知框架更新使用者介面並再次呼叫 build 方法。

在此應用程式中,當使用者進行猜測時,猜測的單詞會儲存在 Game 物件上,該物件是 GamePage 類別的屬性,因此是可能會變更並需要 UI 更新的狀態 (state)。當此狀態被修改時,方格應重新繪製以顯示使用者的猜測。

要實作這一點,請更新傳遞給 GuessInput 的回呼(callback)函式。該函式需要呼叫 setState,並在 setState 內部執行邏輯以判斷使用者的猜測是否正確。

更新你的程式碼:

dart
class GamePage extends StatefulWidget {
  GamePage({super.key});
​
  @override
  State<GamePage> createState() => _GamePageState();
}
​
class _GamePageState extends State<GamePage> {
  final Game _game = Game();
​
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: [
          for (var guess in _game.guesses)
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                for (var letter in guess)
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 2.5, vertical: 2.5),
                    child: Tile(letter.char, letter.type),
                  )
              ],
            ),
          GuessInput(
            onSubmitGuess: (String guess) {
              setState(() { // NEW
                _game.guess(guess);
              });
            },
          ),
        ],
      ),
    );
  }
}

現在,當你在 TextInput 中輸入合法的猜測並提交時,應用程式將反映使用者的猜測。如果你在未呼叫 setState 的情況下呼叫 _game.guess(guess),內部遊戲資料將會改變,但 Flutter 不會知道需要重新繪製螢幕,使用者也不會看到任何更新。

5

將 GuessInput 轉換為具狀態元件

當 GamePage 在呼叫 setState 後重新建置時, 它的所有子元件也會一併重新建置。 由於 GuessInput 最初是以 StatelessWidget 建立的, 它的 TextEditingController 和 FocusNode 直接存放在元件類別中。 每次重新建置都會建立新的元件實例,因此這些控制器每次都會被重新建立, 導致文字輸入欄位失去焦點, 也讓不再使用的控制器沒有被妥善釋放。

為了在每次猜測之間讓文字欄位保持焦點, 並妥善管理控制器的生命週期, 請將 GuessInput 轉換為 StatefulWidget:

  1. 將 GuessInput 改為繼承 StatefulWidget,而非 StatelessWidget。
  2. 建立配套的 _GuessInputState 類別,繼承 State<GuessInput>。
  3. 將 _textEditingController、_focusNode、_onSubmit() 和 build() 移至 _GuessInputState。
  4. 實作 dispose() 以清理 _textEditingController 和 _focusNode。

修改後的 GuessInput 元件應如下所示:

dart
class GuessInput extends StatefulWidget {
  const GuessInput({super.key, required this.onSubmitGuess});
​
  final void Function(String) onSubmitGuess;
​
  @override
  State<GuessInput> createState() => _GuessInputState();
}
​
class _GuessInputState extends State<GuessInput> {
  final TextEditingController _textEditingController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
​
  @override
  void dispose() {
    _textEditingController.dispose();
    _focusNode.dispose();
    super.dispose();
  }
​
  void _onSubmit() {
    widget.onSubmitGuess(_textEditingController.text.trim());
    _textEditingController.clear();
    _focusNode.requestFocus();
  }
​
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              focusNode: _focusNode,
              autofocus: true,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              onSubmitted: (input) {
                _onSubmit();
              },
            ),
          ),
        ),
        IconButton(
          padding: EdgeInsets.zero,
          icon: const Icon(Icons.arrow_circle_up),
          onPressed: _onSubmit,
        ),
      ],
    );
  }
}

將 GuessInput 轉換為 StatefulWidget 後, _GuessInputState 會在父元件重新建置時保持存在, 讓文字欄位在每次提交猜測後維持焦點, 並在元件被卸載時妥善釋放資源。

6

複習

你完成的事項

以下是你在本課程中建置和學習的摘要。
瞭解元件何時需要具有狀態

當元件的外觀或資料需要在其生命週期內改變時,你需要 StatefulWidget。元件本身保持不可變,但其配套的 State 物件保存可變資料並觸發重新建置。

將 GamePage 和 GuessInput 轉換為 StatefulWidget

你透過建立配套的 State 類別、將可變屬性和生命週期管理(例如 dispose)移至其中,以及實作 createState(),將 GamePage 和 GuessInput 重構為具狀態元件。IDE 的快速輔助支援可以自動化此轉換。

使用 setState 讓應用程式回應使用者輸入

呼叫 setState 會告訴 Flutter 重新建置元件的 UI。當使用者提交猜測時,你呼叫 setState 來更新遊戲狀態,方格會自動反映新資料,同時讓文字欄位保持焦點。你的應用程式現在真正具有互動性了!

7

自我測驗

Stateful Widgets Quiz

1 / 2
何時應使用 StatefulWidget 而非 StatelessWidget?
  1. 當元件需要發出 HTTP 請求時。

    Not quite.

    HTTP 請求可以從任一種元件發出,但狀態變更需要 StatefulWidget。

  2. 當元件的外觀或資料需要在其生命週期內改變時。

    That's right!

    當 UI 必須回應隨時間發生的資料變更時,需要使用 StatefulWidget。

  3. 當元件擁有超過三個子元件時。

    Not quite.

    子元件的數量不決定元件是否具有狀態。

  4. 當元件位於元件樹的根部時。

    Not quite.

    根元件可以是無狀態的;是否具有狀態取決於資料是否在元件的生命週期內發生變更。

如果你在 State 物件中變更資料而不呼叫 setState,會發生什麼?
  1. 應用程式將崩潰並出現錯誤。

    Not quite.

    應用程式不會崩潰,但 UI 不會更新。

  2. 資料在內部發生變更,但 Flutter 不會重新建置 UI 以反映變更。

    That's right!

    不呼叫 setState 時,Flutter 不知道需要重新繪製,因此使用者不會看到更新。

  3. Flutter 自動偵測到變更並重新建置 UI。

    Not quite.

    Flutter 需要 setState 才能知道何時重新建置;它不會自動偵測變更。

  4. 元件從元件樹中被移除。

    Not quite.

    元件依然存在;只是在沒有 setState 的情況下不會在視覺上更新。