Flutter

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)

A

Alex Petrov

Senior Flutter Engineer passionate about creating clean, intuitive user experiences.

7 min read10 views

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.

Advertisement

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?

ActionCodeUser 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:

  1. Control the field: Create your own TextEditingController and attach it.
  2. 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!

Tags

You May Also Like