From dc66eeaddbe3213c68ddc5e2b75811ca6f8ef489 Mon Sep 17 00:00:00 2001 From: Muthu Ganesh A Date: Fri, 31 Jul 2026 13:51:45 +0000 Subject: [PATCH 1/3] Converted GuessInput to a StatefulWidget in the Stateful widgets tutorial --- examples/fwe/birdle/lib/main.dart | 18 +++- examples/fwe/birdle/lib/step5_main.dart | 65 +++++++++++- .../learn/pathway/tutorial/stateful-widget.md | 100 +++++++++++++++++- workspace.code-workspace | 8 ++ 4 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 workspace.code-workspace diff --git a/examples/fwe/birdle/lib/main.dart b/examples/fwe/birdle/lib/main.dart index 196d9f680d1..1674ff7d63c 100644 --- a/examples/fwe/birdle/lib/main.dart +++ b/examples/fwe/birdle/lib/main.dart @@ -69,16 +69,28 @@ class _GamePageState extends State { // #enddocregion GamePage // #docregion GuessInput -class GuessInput extends StatelessWidget { - GuessInput({super.key, required this.onSubmitGuess}); +class GuessInput extends StatefulWidget { + const GuessInput({super.key, required this.onSubmitGuess}); final void Function(String) onSubmitGuess; + @override + State createState() => _GuessInputState(); +} + +class _GuessInputState extends State { final TextEditingController _textEditingController = TextEditingController(); final FocusNode _focusNode = FocusNode(); + @override + void dispose() { + _textEditingController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + void _onSubmit() { - onSubmitGuess(_textEditingController.text.trim()); + widget.onSubmitGuess(_textEditingController.text.trim()); _textEditingController.clear(); _focusNode.requestFocus(); } diff --git a/examples/fwe/birdle/lib/step5_main.dart b/examples/fwe/birdle/lib/step5_main.dart index 940f4167a60..6a7ea2df8a1 100644 --- a/examples/fwe/birdle/lib/step5_main.dart +++ b/examples/fwe/birdle/lib/step5_main.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'game.dart'; import 'step2_main.dart' show Tile; -import 'step4_main.dart' show GuessInput; // #docregion GamePage // #docregion StatefulWidget @@ -55,3 +54,67 @@ class _GamePageState extends State { } } // #enddocregion GamePage + +// #docregion GuessInput +class GuessInput extends StatefulWidget { + const GuessInput({super.key, required this.onSubmitGuess}); + + final void Function(String) onSubmitGuess; + + @override + State createState() => _GuessInputState(); +} + +class _GuessInputState extends State { + 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( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 250, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + maxLength: 5, + decoration: const InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(35)), + ), + ), + controller: _textEditingController, + autofocus: true, + focusNode: _focusNode, + onSubmitted: (input) { + _onSubmit(); + }, + ), + ), + ), + IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.arrow_circle_up), + onPressed: _onSubmit, + ), + ], + ); + } +} +// #enddocregion GuessInput + diff --git a/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md b/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md index 13c6d842632..5e7a01bc6c3 100644 --- a/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md +++ b/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md @@ -212,6 +212,96 @@ needs to repaint the screen, and the user wouldn't see any updates. [`setState`]: {{site.api}}/flutter/widgets/State/setState.html +### Convert `GuessInput` to a stateful widget + +When `GamePage` rebuilds after calling `setState`, +all of its child widgets are rebuilt as well. +Because `GuessInput` was originally created as a `StatelessWidget`, +every rebuild creates a new `GuessInput` instance, +along with a new `TextEditingController` and `FocusNode`. +This causes the text input field to lose focus after submitting a guess +and leaves unused controllers without proper disposal. + +To keep focus on the text field between guesses and +manage controller lifecycles properly, +convert `GuessInput` into a `StatefulWidget`: + +1. Change `GuessInput` to extend `StatefulWidget` instead of `StatelessWidget`. +1. Create a companion `_GuessInputState` class extending `State`. +1. Move `_textEditingController`, `_focusNode`, `_onSubmit()`, and `build()` + into `_GuessInputState`. +1. Implement `dispose()` to clean up `_textEditingController` and `_focusNode`. + +Your modified `GuessInput` widget should look like this: + +```dart +class GuessInput extends StatefulWidget { + const GuessInput({super.key, required this.onSubmitGuess}); + + final void Function(String) onSubmitGuess; + + @override + State createState() => _GuessInputState(); +} + +class _GuessInputState extends State { + 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( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 250, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + maxLength: 5, + decoration: const InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(35)), + ), + ), + controller: _textEditingController, + autofocus: true, + focusNode: _focusNode, + onSubmitted: (input) { + _onSubmit(); + }, + ), + ), + ), + IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.arrow_circle_up), + onPressed: _onSubmit, + ), + ], + ); + } +} +``` + +By converting `GuessInput` to a `StatefulWidget`, +`_GuessInputState` persists across parent rebuilds, +keeping focus on the text field after each guess is submitted +and properly disposing of resources when the widget is unmounted. + ### Review @@ -225,12 +315,12 @@ items: When a widget's appearance or data needs to change during its lifetime, you need a `StatefulWidget`. The widget itself stays immutable, but its companion `State` object holds mutable data and triggers rebuilds. - - title: Converted GamePage to a StatefulWidget + - title: Converted GamePage and GuessInput to StatefulWidgets icon: swap_horiz details: >- - You refactored `GamePage` to be stateful by - creating a companion `_GamePageState` class, moving the - `build` method and mutable properties to it, and + You refactored `GamePage` and `GuessInput` to be stateful by + creating companion `State` classes, moving mutable properties and + lifecycle management (like `dispose`) to them, and implementing `createState()`. Your IDE's support for quick assists can automate this conversion. - title: Made your app respond to user input with setState @@ -238,7 +328,7 @@ items: details: >- Calling `setState` tells Flutter to rebuild the UI of a widget. When a user submits a guess, you call `setState` to update the game state, - and the grid automatically reflects the new data. + and the grid automatically reflects the new data while maintaining text field focus. Your app is now truly interactive! diff --git a/workspace.code-workspace b/workspace.code-workspace new file mode 100644 index 00000000000..876a1499c09 --- /dev/null +++ b/workspace.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file From 00ccd14c2f3ad8fe4daa1bc578aedf16f2e49d90 Mon Sep 17 00:00:00 2001 From: Muthu Ganesh A Date: Wed, 5 Aug 2026 06:16:04 +0000 Subject: [PATCH 2/3] Remove workspace.code-workspace --- workspace.code-workspace | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 workspace.code-workspace diff --git a/workspace.code-workspace b/workspace.code-workspace deleted file mode 100644 index 876a1499c09..00000000000 --- a/workspace.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": {} -} \ No newline at end of file From 9e52d594acf3da0f2b5ec3046da55522b9bbb390 Mon Sep 17 00:00:00 2001 From: Muthu Ganesh A Date: Wed, 5 Aug 2026 07:26:23 +0000 Subject: [PATCH 3/3] Use code-excerpt tag for GuessInput StatefulWidget snippet in tutorial --- sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md b/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md index 5e7a01bc6c3..5c8562503d9 100644 --- a/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md +++ b/sites/docs/src/content/learn/pathway/tutorial/stateful-widget.md @@ -234,6 +234,7 @@ convert `GuessInput` into a `StatefulWidget`: Your modified `GuessInput` widget should look like this: + ```dart class GuessInput extends StatefulWidget { const GuessInput({super.key, required this.onSubmitGuess});