TextField in Flutter

TextField and TextFormField in Flutter: Complete Tutorial (2026) | D.Tech Academic

TextField and TextFormField in Flutter: Complete Tutorial (2026)

Last Updated: | Author: Deep Singh

Almost every app needs some kind of text input — a login form, a search box, a signup page, a comment section. Flutter gives you two widgets for this: TextField and TextFormField. They look similar, but they serve slightly different purposes.

In this tutorial, you'll learn how to use both. We'll build a small form with a name field, an email field with validation, and a password field with a show/hide toggle. By the end, you'll know exactly when to reach for each one.

TextField vs TextFormField: What's the Difference?

Both widgets let users type text, but they differ in one important way:

  • TextField is the basic input widget. It doesn't validate anything on its own. You handle validation manually.
  • TextFormField is a TextField wrapped in form-friendly plumbing. It works with the Form widget and supports a validator callback, which means you can validate multiple fields at once by calling formKey.currentState!.validate().

The rule of thumb: use TextField for simple standalone inputs (like a search bar), and TextFormField when you're building a form with multiple fields that need validation.

Flutter TextField with label, hint, and prefix icon
TextField with a prefix icon
Flutter TextFormField validation error message
TextFormField showing a validation error
Flutter password field with visibility toggle icon
Password field with show/hide toggle
Flutter form validation success after all fields are correct
Form submitted successfully

The Full Code

Here's a complete working example. It has two sections: a simple TextField for name input, and a Form containing two TextFormFields for email and password with validation.

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    home: MyFieldTutorial(),
    title: 'TextField Tutorial',
  ));
}

class MyFieldTutorial extends StatefulWidget {
  const MyFieldTutorial({super.key});

  @override
  State<MyFieldTutorial> createState() => _MyFieldTutorialState();
}

class _MyFieldTutorialState extends State<MyFieldTutorial> {
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passController = TextEditingController();
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  bool _passwordVisible = false;

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _passController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TextField Tutorial'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            const SizedBox(height: 20),
            const Text(
              'TextField Example',
              style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),

            // A plain TextField (no validation built in)
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 20),
              child: TextField(
                controller: _nameController,
                keyboardType: TextInputType.text,
                textInputAction: TextInputAction.next,
                decoration: InputDecoration(
                  labelText: 'Enter Name',
                  hintText: 'Enter Name',
                  prefixIcon: const Icon(Icons.person),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                if (_nameController.text.isEmpty) {
                  debugPrint('Please enter name');
                } else {
                  debugPrint('Name: ${_nameController.text}');
                }
              },
              child: const Text('Submit Name'),
            ),

            const SizedBox(height: 40),
            const Text(
              'TextFormField with Validation',
              style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),

            // A Form with two TextFormFields
            Form(
              key: _formKey,
              child: Column(
                children: [
                  Container(
                    margin: const EdgeInsets.symmetric(horizontal: 20),
                    child: TextFormField(
                      controller: _emailController,
                      keyboardType: TextInputType.emailAddress,
                      textInputAction: TextInputAction.next,
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter email';
                        }
                        if (!value.contains('@')) {
                          return 'Please enter a valid email';
                        }
                        return null;
                      },
                      decoration: InputDecoration(
                        labelText: 'Enter Email',
                        hintText: 'Enter Email',
                        prefixIcon: const Icon(Icons.email),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(8),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 20),

                  Container(
                    margin: const EdgeInsets.symmetric(horizontal: 20),
                    child: TextFormField(
                      controller: _passController,
                      obscureText: !_passwordVisible,
                      textInputAction: TextInputAction.done,
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter password';
                        }
                        if (value.length < 6) {
                          return 'Password must be at least 6 characters';
                        }
                        return null;
                      },
                      decoration: InputDecoration(
                        labelText: 'Enter Password',
                        hintText: 'Enter Password',
                        prefixIcon: const Icon(Icons.lock),
                        suffixIcon: IconButton(
                          icon: Icon(
                            _passwordVisible
                                ? Icons.visibility
                                : Icons.visibility_off,
                          ),
                          onPressed: () {
                            setState(() {
                              _passwordVisible = !_passwordVisible;
                            });
                          },
                        ),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(8),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 20),

                  ElevatedButton(
                    onPressed: () {
                      if (_formKey.currentState!.validate()) {
                        debugPrint('Email: ${_emailController.text}');
                        debugPrint('Password: ${_passController.text}');
                        _formKey.currentState!.reset();
                        _emailController.clear();
                        _passController.clear();
                      }
                    },
                    child: const Text('Submit Form'),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 40),
          ],
        ),
      ),
    );
  }
}

How the Code Works

Let's break down the important pieces:

1. TextEditingController

Each input field has its own TextEditingController. The controller lets you read the current value (controller.text), set it programmatically, and clear it. Remember to dispose of them in dispose() to avoid memory leaks.

2. TextField vs TextFormField

The name field uses a plain TextField — no validation, just a simple input. The email and password fields use TextFormField, which supports the validator callback.

3. The Form Key

The GlobalKey<FormState> gives you access to the form's state. When you call _formKey.currentState!.validate(), Flutter runs the validator on every field in the form and returns true only if all validators pass.

4. Validators

A validator is a function that takes the current text and returns either null (meaning the value is valid) or a string (the error message to show). We check for empty input and, for the email, a basic @ check.

5. Password Visibility Toggle

The password field uses obscureText: !_passwordVisible to hide or show the text. The suffix IconButton flips the _passwordVisible variable, which triggers a rebuild with the new visibility state. We also swap the icon between visibility and visibility_off.

6. Clearing the Form

After a successful submit, we call _formKey.currentState!.reset() to clear the validation errors. Note that reset() doesn't clear the text controllers — you have to clear those manually with .clear().

Validation Tips

A few things to keep in mind when working with forms:

  • Don't validate on every keystroke. By default, TextFormField validates when the form is submitted or when the user leaves the field. You can change this with autovalidateMode, but for most forms the default behaviour is fine.
  • Use textInputAction. Setting TextInputAction.next on all but the last field lets the user jump between fields by pressing the keyboard's "next" button.
  • Set the right keyboardType. Use TextInputType.emailAddress for email fields, TextInputType.number for numeric input, and so on. This shows the right keyboard on mobile.
  • Wrap your form in a SingleChildScrollView. On small screens, the keyboard can cover your input fields. Wrapping the column in a scroll view prevents overflow errors.
  • Dispose of controllers. Failing to dispose a TextEditingController leaks memory. Always clean up in dispose().

Wrapping Up

You now know how to use both TextField and TextFormField in Flutter. Use TextField for simple inputs and TextFormField when you need validation. With a Form and a GlobalKey<FormState>, you can validate all your fields with a single call.

From here, you can extend this further with features like form autofill, custom input formatters (for phone numbers or credit cards), and focus node management for smooth keyboard navigation.

No comments: