Flutter tutorials and courses by Andrea Bizzotto

Flutter TextField validation made easy with TextEditingController and AnimatedBuilder

Ever needed to use a TextField and validate the text on the fly as the user types?

On the fly TextField validation

In this example, we show a custom error hint and disable the submit button if the text is empty or too short.

If you wanted to implement this functionality in Flutter, how would you do it?


People on StackOverflow seem to have many opinions about it, but they are all wrong. 😎

Some say you should use a Form and a TextFormField along with a GlobalKey to validate and save the text field data (this cookbook explains how). Though that is overkill in our case as we only have one text field.

If you thought about TextEditingController you're on the right track, but there are some caveats to be aware of.

So let's get this right and figure out all the details. 👍

Basic UI with TextField and ElevatedButton

Step one is to create a StatefulWidget subclass that will contain both our TextField and the submit button:

class TextSubmitWidget extends StatefulWidget { const TextSubmitWidget({Key? key, required this.onSubmit}) : super(key: key); final ValueChanged<String> onSubmit; @override State<TextSubmitWidget> createState() => _TextSubmitWidgetState(); }

Note how we added an onSubmit callback. We will use this to inform the parent widget when the user presses the "Submit" button upon entering a valid text.

Next, let's create the State subclass:

class _TextSubmitWidgetState extends State<TextSubmitWidget> { @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( decoration: InputDecoration( labelText: 'Enter your name', // TODO: add errorHint ), ), ElevatedButton( // TODO: implement callback onPressed: () {}, child: Text( 'Submit', style: Theme.of(context).textTheme.headline6, ), ) ], ); } }

This is a simple Column layout that contains a TextField and an ElevatedButton.

If we run this code inside a single-page Flutter app, both the text field and the submit button will show. But there is no text validation and the button is always enabled:

Flutter TextField without validation

How can we address this?

Adding a TextEditingController

Flutter gives us a TextEditingController class that we can use to control our text field.

So let's use it. All we have to do is to create it inside the State subclass:

class _TextSubmitWidgetState extends State<TextSubmitWidget> { // create a TextEditingController final _controller = TextEditingController(); // dispose it when the widget is unmounted @override void dispose() { _controller.dispose(); super.dispose(); } ... }

And then we can pass it to our TextField:

TextField( // use this to control the text field controller: _controller, decoration: InputDecoration( labelText: 'Enter your name', ), ),

We can also add a getter variable to control the errorText that we pass to the TextField:

String? get errorText { // at any time, we can get the text from _controller.value.text final text = _controller.value.text; // Note: you can do your own custom validation here // Move this logic this outside the widget for more testable code if (text.isEmpty) { return 'Can\'t be empty'; } if (text.length < 4) { return 'Too short'; } // return null if the text is valid return null; } // then, in the build method: TextField( controller: _controller, decoration: InputDecoration( labelText: 'Enter your name', // use the getter variable defined above errorText: errorText, ), ),

With this in place, we can add some custom logic to the onPressed callback inside our button:

ElevatedButton( // only enable the button if errorText == null onPressed: errorText == null ? () => widget.onSubmit(_controller.value.text) : null, child: Text( 'Submit', style: Theme.of(context).textTheme.headline6, ), )

With this code, we can enable the button by passing a non-null callback only if errorText == null.

But if we run this code, the TextField is always showing the error text and the submit button remains disabled, even if we enter a valid text:

TextField and Submit button not updating

What's the deal? 🧐

Widget rebuilds and setState()

The problem is that we are not telling Flutter to rebuild our widget when the text changes.

We could fix this by adding a local state variable and updating it with a call to setState() inside the onChanged callback of our TextField:

// In the state class var _text = ''; // inside the build method: TextField( controller: _controller, decoration: InputDecoration( labelText: 'Enter your name', errorText: errorText, ), // this will cause the widget to rebuild whenever the text changes onChanged: (text) => setState(() => _text), ),

With this change our UI updates on the fly and behaves as expected:

Flutter TextField validation now working

But a local state variable is not necessary because our TextEditingController already holds the text value as it changes.

As a proof of this, we could make an empty call to setState() and everything would still work:

onChanged: (_) => setState(() {}),

But forcing a widget rebuild like this seems a bit of an anti-pattern. There must be a better way.

Flutter TextField validation with TextEditingController and AnimatedBuilder

As it turns out, we can wrap our widget tree with an AnimatedBuilder that takes our TextEditingController as an argument:

@override Widget build(BuildContext context) { return AnimatedBuilder( // Note: pass _controller to the animation argument animation: _controller, builder: (context, _) { // this entire widget tree will rebuild every time // the controller value changes return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: _controller, decoration: InputDecoration( labelText: 'Enter your name', // the errorText getter *depends* on _controller errorText: errorText, ), ), ElevatedButton( // the errorText getter *depends* on _controller onPressed: errorText == null ? () => widget.onSubmit(_controller.value.text) : null, child: Text( 'Submit', style: Theme.of(context).textTheme.headline6, ), ) ], ); }, ); }

As a result, both the TextField and ElevatedButton will rebuild when the text changes:

Flutter TextField validation still working

But why are we allowed to pass our TextEditingController to the AnimatedBuilder?

AnimatedBuilder( // this is valid because TextEditingController implements Listenable animation: _controller, builder: (context, _) { ... } )

Well, AnimatedBuilder takes an animation argument of type Listenable.

And TextEditingController extends ValueNotifier, which extends ChangeNotifier, which implements Listenable. This is how these classes are defined in the Flutter SDK:

class TextEditingController extends ValueNotifier<TextEditingValue> { ... } class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { ... } class ChangeNotifier implements Listenable { ... }

Conclusion

Flutter is a declarative framework, and this makes it a bit hard to work with text input.

For simple use cases, we can use TextEditingController to get and set the value of a TextField as a lightweight alternative to Form and TextFormField.

And if we want our widgets to rebuild when the text changes, we can wrap them with an AnimatedBuilder that takes the TextEditingController as an argument.

In fact, here's a very useful tip about AnimatedBuilder:

While AnimatedBuilder is most commonly used with AnimationController, you can use it anytime you want to rebuild your widgets when a Listenable value changes. This works with ValueNotifier, custom ChangeNotifier subclasses, as well as TextEditingController.

The finished TextSubmitWidget widget we created takes just 60 lines of code, and you can find the entire example app in this gist.

Happy coding!

Want more?

Fast-track your Flutter learning with over 40 hours of in-depth content.