Implementing Search by Character in Flutter: A Complete Guide with Examples
Searching is one of the most common features in mobile applications. Whether you're building a contact list, product catalog, employee directory, or chat application, users expect search results to update instantly as they type.
Flutter makes it easy to implement search by character, where the list filters after every keystroke without requiring users to tap a search button.
In this guide, you'll learn how to implement a fast, efficient, and scalable character-by-character search in Flutter.
What is Search by Character?
Search by character (also called live search or incremental search) filters data every time the user types a new character.
For example:
User Types Results
----------------------------------------
A Apple, Amazon, Adobe
Ap Apple
App Apple
Appl Apple
Apple Apple
Instead of waiting for the user to press Search, the UI updates automatically.
When Should You Use It?
Character search is ideal for:
Product catalogs
Contact lists
Employee directories
Chat users
Countries list
Cities list
Music playlists
Restaurant menus
Shopping apps
How It Works
The search process is straightforward:
User types into a
TextField.Flutter captures the text using
onChanged.The app filters the list.
The UI rebuilds with matching results.
User Types
│
▼
TextField
│
▼
onChanged()
│
▼
Filter List
│
▼
Update UI
Sample Data
Let's start with a simple list of fruits.
final List<String> fruits = [
'Apple',
'Banana',
'Orange',
'Mango',
'Pineapple',
'Watermelon',
'Kiwi',
'Grapes',
'Strawberry',
];
Create the Search Screen
Create a new StatefulWidget.
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@override
State<SearchPage> createState() => _SearchPageState();
}
Create a Filtered List
Maintain two lists:
List<String> fruits = [...];
List<String> filteredFruits = [];
Initialize the filtered list:
@override
void initState() {
super.initState();
filteredFruits = fruits;
}
Add a Search TextField
Flutter provides the onChanged callback, which is triggered after every keystroke.
TextField(
decoration: const InputDecoration(
hintText: "Search fruits",
prefixIcon: Icon(Icons.search),
),
onChanged: (value) {
searchFruit(value);
},
)
Implement the Search Function
Use the where() method to filter the list.
void searchFruit(String keyword) {
final results = fruits.where((fruit) {
return fruit
.toLowerCase()
.contains(keyword.toLowerCase());
}).toList();
setState(() {
filteredFruits = results;
});
}
This implementation is:
Case-insensitive
Easy to understand
Efficient for small and medium-sized lists
Display Search Results
Use a ListView.builder to display the filtered list.
Expanded(
child: ListView.builder(
itemCount: filteredFruits.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(filteredFruits[index]),
);
},
),
)
The list updates automatically as the user types.
Complete Example
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
final List<String> fruits = [
'Apple',
'Banana',
'Orange',
'Mango',
'Pineapple',
'Kiwi',
'Strawberry',
'Watermelon',
];
late List<String> filteredFruits;
@override
void initState() {
super.initState();
filteredFruits = fruits;
}
void searchFruit(String keyword) {
setState(() {
filteredFruits = fruits.where((fruit) {
return fruit
.toLowerCase()
.contains(keyword.toLowerCase());
}).toList();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Character Search"),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
decoration: const InputDecoration(
hintText: "Search...",
prefixIcon: Icon(Icons.search),
),
onChanged: searchFruit,
),
),
Expanded(
child: ListView.builder(
itemCount: filteredFruits.length,
itemBuilder: (_, index) {
return ListTile(
title: Text(filteredFruits[index]),
);
},
),
)
],
),
);
}
}
Searching Objects Instead of Strings
In real-world apps, you'll usually search objects.
class Employee {
final String name;
final String department;
Employee(this.name, this.department);
}
Filter by name:
employees.where((employee) {
return employee.name
.toLowerCase()
.contains(keyword.toLowerCase());
}).toList();
You can also search multiple fields:
employees.where((employee) {
return employee.name
.toLowerCase()
.contains(keyword.toLowerCase())
||
employee.department
.toLowerCase()
.contains(keyword.toLowerCase());
}).toList();
Improve Performance with Debouncing
If your search calls an API, avoid sending a request for every keystroke. Use debouncing to wait until the user pauses typing.
Timer? _debounce;
void onSearchChanged(String query) {
if (_debounce?.isActive ?? false) {
_debounce!.cancel();
}
_debounce = Timer(
const Duration(milliseconds: 500),
() {
searchApi(query);
},
);
}
This reduces unnecessary network requests and improves performance.
Show a "No Results" Message
Handle empty results gracefully.
if (filteredFruits.isEmpty) {
return const Center(
child: Text("No matching results found"),
);
}
Providing feedback improves the user experience.
Best Practices
Convert both the search query and the data to lowercase for case-insensitive matching.
Use debouncing for API-based searches.
Keep the original list unchanged and filter into a separate list.
Display a loading indicator for network searches.
Show a clear message when no results are found.
Dispose of timers or controllers to avoid memory leaks.
Common Mistakes
Avoid these common pitfalls:
Modifying the original list directly.
Ignoring case sensitivity.
Calling APIs on every keystroke without debouncing.
Not handling empty search results.
Forgetting to dispose of
TextEditingControllerorTimer.
Conclusion
Character-by-character search is a simple yet powerful feature that significantly improves user experience. Flutter's TextField, onChanged, and collection methods like where() make it easy to implement responsive search functionality.
For local data, filtering a list in memory is usually sufficient. For large datasets or remote APIs, combine live search with debouncing or pagination to build a scalable and efficient search experience.
With these techniques, you can create fast, intuitive search experiences for almost any Flutter application.
Comments
Post a Comment