A text input is a widget that allows the user to enter and edit text.
We already know how to touch the screen, now we will learn to write on it.
The virtual keyboard is the main data input method on a mobile device. Whether for a login, a search bar, or a chat, we need to master the TextField widget.
Unlike a simple button, a text field has a lot of internal complexity: keyboard management, cursor position, text selection, password obscuring… Luckily, Flutter does (almost) everything for us.
The TextField widget
In its most basic form, it is a component that, when touched, brings up the system keyboard.
TextField() // As simple as that, it already worksBut of course, this is just an underlined line. To make it usable, we need to decorate it and, most importantly, read what the user writes.
Capturing text with TextEditingController
There are two ways to read the text: the onChanged callback and the controller.
onChanged: Notifies you every time the user presses a key. It’s useful for real-time search bars.TextEditingController: allows you to read the text whenever you want (for example, when pressing Save) and also modify it from code.
Implementing the controller
The controller is an object that must live throughout the entire Widget lifecycle. Therefore, we need a StatefulWidget.
class SimpleForm extends StatefulWidget {
@override
_SimpleFormState createState() => _SimpleFormState();
}
class _SimpleFormState extends State<SimpleForm> {
// 1. Instantiate the controller
final TextEditingController _emailController = TextEditingController();
@override
void dispose() {
// 2. IMPORTANT! Clean up memory when exiting
_emailController.dispose();
super.dispose();
}
void _sendData() {
// 3. Access the current text with .text
print("Email entered: ${_emailController.text}");
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _emailController, // 4. Link it to the widget
decoration: InputDecoration(labelText: "Email"),
),
ElevatedButton(onPressed: _sendData, child: Text("Send"))
],
);
}
}Watch out for memory: A TextEditingController stays listening to native events. If you forget to call dispose() when the screen closes, you will create a Memory Leak.
Decoration: InputDecoration
The default appearance depends on the app’s Material theme. To add borders, icons, and helper text, we use the decoration property with the InputDecoration class.
Here are the properties you will use 90% of the time:
TextField(
decoration: InputDecoration(
// Label that floats above on focus
labelText: "Username",
// Helper text (placeholder) that disappears when typing
hintText: "E.g., John Doe",
// Icon on the left
prefixIcon: Icon(Icons.person),
// Icon on the right (useful for clearing text or viewing password)
suffixIcon: Icon(Icons.check_circle),
// Full border (box)
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)
),
),
)Keyboard type and action
There is nothing more frustrating than being asked for a phone number and getting the full alphabet keyboard.
We can (and should) help the user by configuring the keyboard type (keyboardType) and the action of the “Enter” button (textInputAction).
TextField(
// Shows number pad, email keyboard, URL keyboard, etc.
keyboardType: TextInputType.emailAddress,
// Changes the "Enter" button to "Next", "Search", "Send"...
textInputAction: TextInputAction.next,
// Hides the text (dots) for passwords
obscureText: true,
)Focus management with FocusNode
The “Focus” determines which text field is active and receiving keyboard input.
Sometimes we want to manage this manually. The typical use case is: “When the user presses ‘Next’ in the Email field, I want the focus to automatically jump to the Password field”.
For this we use FocusNode.
class AdvancedForm extends StatefulWidget {
@override
_AdvancedFormState createState() => _AdvancedFormState();
}
class _AdvancedFormState extends State<AdvancedForm> {
// Focus nodes
final FocusNode _nameFocus = FocusNode();
final FocusNode _passFocus = FocusNode();
@override
void dispose() {
// They also need to be cleaned up
_nameFocus.dispose();
_passFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
focusNode: _nameFocus, // Assign node 1
textInputAction: TextInputAction.next, // "Next" button
onSubmitted: (_) {
// On enter/next press, request focus for node 2
FocusScope.of(context).requestFocus(_passFocus);
},
),
TextField(
focusNode: _passFocus, // Assign node 2
),
],
);
}
}The trick to close the keyboard
An expected behavior on mobile is that if you tap on an empty space outside the keyboard, it closes. In Flutter, this does not happen by default.
We have to implement it ourselves by wrapping the screen in a GestureDetector:
GestureDetector(
onTap: () {
// This removes focus from any input, closing the keyboard
FocusScope.of(context).unfocus();
},
child: Scaffold( ... ),
)