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..5c8562503d9 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,97 @@ 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 +316,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 +329,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!