Flutter: Clear Autocomplete on Select in 2 Easy Steps (2025)
Tired of the Autocomplete text field not clearing in Flutter? Learn how to clear the input on selection in 2 easy steps using a TextEditingController. (2025)
Alex Petrov
Senior Flutter Engineer passionate about creating clean, intuitive user experiences.
Ever feel like you're fighting with Flutter's own widgets?
You've meticulously crafted a beautiful search interface or a slick tagging system using Flutter's Autocomplete
widget. It looks great, it feels responsive, and the suggestions pop up perfectly. A user types, finds what they're looking for, taps an option, and... the text just sits there. Staring back. Mocking you.
If you've ever felt that twinge of frustration, you're not alone. The default behavior of the Autocomplete
widget is to keep the selected value in the text field. While this makes sense for a simple search bar, it's a real user experience killer for things like tag inputs, command palettes, or any scenario where you're adding multiple items to a list. The user is forced to manually delete the text before they can add the next item. Clunky, right?
Well, what if I told you that fixing this is incredibly simple? Today, we're going to transform that clunky workflow into a seamless, professional experience in just two easy steps. Let's dive in.
Why Clear the Autocomplete Field Anyway?
Before we jump into the code, let's appreciate why this is so important. A great user experience is often invisible; it's the absence of friction. Forcing a user to perform a repetitive, unnecessary action (like deleting text) is a classic example of UX friction. Clearing the field automatically is crucial for a few common patterns:
- Tagging Systems: Imagine adding tags to a blog post or a photo. You want to type a tag, select it, and immediately be ready to type the next one. You're building a list, not just filling a single field.
- Multi-Item Selection: If you're building a feature to add multiple users to a project or ingredients to a recipe, the flow is the same. Select an item, and the input should be clear and ready for the next.
- Command Palettes: In tools like VS Code or Slack, you type a command, select it, and the action executes. The command palette often clears or closes, ready for the next interaction. The text doesn't linger.
In all these cases, the text field's purpose is temporary—it's a tool for finding and selecting, not for displaying the final result. Our goal is to make the tool behave accordingly.
The Core Problem: Understanding State
So, why doesn't Autocomplete
just have a clearOnSelect
property? The answer lies in Flutter's design philosophy. The Autocomplete
widget is designed to be flexible and unopinionated. It doesn't own the state of the text field it's associated with. It simply provides the logic for displaying options based on the current input.
The text field itself, which you provide via the fieldViewBuilder
, is a separate entity. To programmatically change the content of a text field in Flutter, you need to control it externally. And the classic way to do that is with a TextEditingController
.
Once you realize this, the solution becomes obvious. We don't need to fight the widget; we just need to give it the tools to manage its own state correctly.
The 2-Step Solution to Clearing on Select
Alright, let's get to the good stuff. Here’s the simple, two-step process to make your Autocomplete
widget behave exactly as you want.
Step 1: Introduce a TextEditingController
First, we need a way to talk to the text field. We'll create a TextEditingController
and hold it in the state of our widget. This controller will be our remote control for the text field.
Inside your StatefulWidget
's state class, declare the controller:
// In your State class
final TextEditingController _textEditingController = TextEditingController();
@override
void dispose() {
_textEditingController.dispose(); // Don't forget to dispose of the controller!
super.dispose();
}
It's crucial to initialize it and, more importantly, dispose of it when the widget is removed from the tree to prevent memory leaks.
Next, pass this controller to the TextFormField
(or TextField
) that you return from the fieldViewBuilder
in your Autocomplete
widget.
Autocomplete(
// ... other properties
fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) {
// IMPORTANT: We are NOT using the controller provided by the builder.
// We are using our OWN controller.
return TextFormField(
controller: _textEditingController, // Use our controller
focusNode: focusNode,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
decoration: const InputDecoration(
hintText: 'Add a tag',
),
);
},
// ... other properties
);
Wait, a crucial detail! Notice that the fieldViewBuilder
actually gives you its own textEditingController
. We are going to ignore it and use the one we created ourselves. This is the key to taking control.
Step 2: Use the `onSelected` Callback
Now that we have control over the text field, we just need the trigger. The Autocomplete
widget provides the perfect hook for this: the onSelected
callback.
This function is called whenever the user taps on one of the options from the list. Inside this callback, we'll simply tell our controller to clear its text.
Autocomplete(
optionsBuilder: (TextEditingValue textEditingValue) {
// ... your filtering logic
},
onSelected: (String selection) {
// This is the magic!
_textEditingController.clear();
// You can now do something with the selected value, like adding it to a list.
debugPrint('You just selected $selection');
},
fieldViewBuilder: // ... as defined in Step 1
);
And... that's it. Seriously. With those two changes, your UI will now work as intended. The user selects an item, your onSelected
function fires, you process the value, and you immediately clear the controller, which in turn clears the text field, making it ready for the next input. Smooth.
Putting It All Together: A Complete Code Example
Let's see how this looks in a complete, runnable example. Here's a simple widget that simulates adding programming languages as tags to a list.
import 'package:flutter/material.dart';
class TaggingInput extends StatefulWidget {
const TaggingInput({super.key});
@override
State createState() => _TaggingInputState();
}
class _TaggingInputState extends State {
// The controller for the text field
final TextEditingController _textEditingController = TextEditingController();
// The list of all possible options
static const List _options = [
'Dart',
'Python',
'JavaScript',
'Kotlin',
'Swift',
'Go',
'Rust',
];
// The list of selected tags
final List _selectedTags = [];
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Autocomplete(
// Step 1: Pass our own controller
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
// We're using our own controller, so we can ignore the one from the builder.
return TextFormField(
controller: _textEditingController,
focusNode: focusNode,
decoration: const InputDecoration(
labelText: 'Add a language tag',
border: OutlineInputBorder(),
),
onFieldSubmitted: (value) {
// This is called when the user presses enter.
onFieldSubmitted();
},
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable.empty();
}
return _options.where((String option) {
return option.toLowerCase().contains(textEditingValue.text.toLowerCase());
});
},
onSelected: (String selection) {
// Step 2: Clear the controller and update state
setState(() {
_selectedTags.add(selection);
_textEditingController.clear();
});
debugPrint('Selected: $selection');
},
),
const SizedBox(height: 16),
Text('Selected Tags:', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
// Display the selected tags
Wrap(
spacing: 8.0,
runSpacing: 4.0,
children: _selectedTags.map((String tag) {
return Chip(
label: Text(tag),
onDeleted: () {
setState(() {
_selectedTags.remove(tag);
});
},
);
}).toList(),
),
],
),
);
}
}
Bonus Round: Managing Focus and the Keyboard
You've cleared the text, but what about the cursor and the keyboard? Depending on the desired UX, you might want to either keep the focus in the text field (so the user can immediately type again) or remove it (to dismiss the keyboard).
This is where FocusNode
comes in. Just like the controller, we can create our own FocusNode
to manage the focus state.
1. **Create and dispose of a FocusNode:**
final FocusNode _focusNode = FocusNode();
@override
void dispose() {
_textEditingController.dispose();
_focusNode.dispose(); // Also dispose the focus node
super.dispose();
}
2. **Attach it in the `fieldViewBuilder`:**
// ...
return TextFormField(
controller: _textEditingController,
focusNode: _focusNode, // Attach our focus node
// ...
);
3. **Control it in `onSelected`:**
Now you have a choice. After clearing the text, what should happen?Action | Code | User Experience |
---|---|---|
Keep Focus | _focusNode.requestFocus(); | The keyboard stays open, and the cursor is blinking in the empty field, ready for the next tag. Best for rapid-fire input. |
Remove Focus | _focusNode.unfocus(); | The keyboard dismisses. The user sees the list of selected tags and must tap the field again to add another. Better if adding a tag is a less frequent action. |
Simply add one of those lines in your onSelected
callback after clearing the controller:
onSelected: (String selection) {
setState(() {
_selectedTags.add(selection);
_textEditingController.clear();
// Option 1: Keep focus for fast entry
_focusNode.requestFocus();
// Option 2: Unfocus to dismiss keyboard
// _focusNode.unfocus();
});
},
This level of control allows you to fine-tune the interaction to perfectly match your app's flow.
Conclusion: Clean UX is Just a Controller Away
And there you have it. What seemed like a frustrating limitation of the Autocomplete
widget is actually an opportunity to take explicit control over your UI's state. By leveraging a TextEditingController
and the onSelected
callback, you can easily implement the desired "clear on select" behavior.
Remember the two simple steps:
- Control the field: Create your own
TextEditingController
and attach it. - React to selection: Use the
onSelected
callback to call_textEditingController.clear()
.
This small pattern is a powerful reminder of how Flutter's stateful nature, while sometimes requiring a bit more boilerplate, ultimately gives you fine-grained control to build polished, intuitive, and friction-free user experiences. Happy coding!