flutter-textfield-controller-focus

Text fields in Flutter: controller, decoration and focus

  • 4 min

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 works
Copied!

But 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"))
      ],
    );
  }
}
Copied!

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)
    ),
  ),
)
Copied!

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, 
)
Copied!

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
        ),
      ],
    );
  }
}
Copied!

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( ... ),
)
Copied!