Let’s go step by step on how to implement an autocomplete text field in Flutter without any plugins, using only built-in widgets. I’ll show you two main approaches:
- ✅ With preloaded items (local list) — simplest version.
- 🌐 With backend fetching (async suggestions) — slightly more advanced.
🧰 1. Autocomplete with Preloaded Items (Local List)
Flutter has a built-in widget: RawAutocomplete (and a higher-level wrapper: Autocomplete<T>).
Here’s a simple example:
import 'package:flutter/material.dart';
class LocalAutocompleteExample extends StatelessWidget {
final List<String> countries = [
'USA', 'Canada', 'Mexico', 'Germany', 'France', 'Italy', 'India', 'Japan'
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Local Autocomplete')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<String>.empty();
}
return countries.where((String option) {
return option.toLowerCase().contains(textEditingValue.text.toLowerCase());
});
},
onSelected: (String selection) {
print('You selected: $selection');
},
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
return TextField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'Country',
border: OutlineInputBorder(),
),
);
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final String option = options.elementAt(index);
return ListTile(
title: Text(option),
onTap: () => onSelected(option),
);
},
),
),
);
},
),
),
);
}
}
✅ What this does:
- Uses Flutter’s built-in
Autocompletewidget. - Filters a local list (
countries) as you type. - Shows suggestions in a dropdown.
- No plugins needed. Always trive to nimize external dependencies – https://programtom.com/dev/2023/09/02/strengths-and-weaknesses-of-flutter-ai-will-not-tell-you/
🌐 2. Autocomplete with Backend Fetching (Async Suggestions)
If you want to fetch suggestions from an API as the user types, you can still use RawAutocomplete or a custom solution.
Here’s a pattern using a TextField + FutureBuilder:
import 'dart:async';
import 'package:flutter/material.dart';
class AsyncAutocompleteExample extends StatefulWidget {
@override
_AsyncAutocompleteExampleState createState() => _AsyncAutocompleteExampleState();
}
class _AsyncAutocompleteExampleState extends State<AsyncAutocompleteExample> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
List<String> _suggestions = [];
Timer? _debounce;
@override
void initState() {
super.initState();
_controller.addListener(_onSearchChanged);
}
@override
void dispose() {
_debounce?.cancel();
_controller.dispose();
super.dispose();
}
void _onSearchChanged() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 300), () {
if (_controller.text.isNotEmpty) {
_fetchSuggestions(_controller.text);
} else {
setState(() => _suggestions = []);
}
});
}
Future<void> _fetchSuggestions(String query) async {
// TODO: Replace with your real API call
await Future.delayed(Duration(milliseconds: 500)); // simulate network delay
setState(() {
_suggestions = List.generate(5, (i) => "$query suggestion $i");
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Async Autocomplete')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller,
focusNode: _focusNode,
decoration: InputDecoration(
labelText: 'Search',
border: OutlineInputBorder(),
),
),
if (_suggestions.isNotEmpty && _focusNode.hasFocus)
Material(
elevation: 4,
child: ListView.builder(
shrinkWrap: true,
itemCount: _suggestions.length,
itemBuilder: (context, index) {
final suggestion = _suggestions[index];
return ListTile(
title: Text(suggestion),
onTap: () {
_controller.text = suggestion;
setState(() => _suggestions = []);
_focusNode.unfocus();
},
);
},
),
)
],
),
),
);
}
}
✅ What’s happening here:
- A
TextFieldlistens for changes. - Debounce prevents too many API calls while typing.
_fetchSuggestionssimulates a backend request (replace with your real API call).- Suggestions are shown below the field.
- Tap a suggestion → fills the field and hides the list.
🧠 Pro Tips
- Use
FocusNodeto hide suggestions when the field is not focused. - Wrap the suggestions list in a
Materialfor proper elevation/shadow. - Add a
Containerwith a max height to prevent overflow. - For performance, debounce your API calls as shown.
