How to Implement a Functional Search Bar in Flutter that Filters a ListView Dynamically
Use a TextEditingController with a listener to filter your data list on every keystroke, or implement SearchDelegate for a full-screen search experience with built-in animations and focus management.
Flutter's Material library provides robust infrastructure for building a functional search bar in Flutter applications. Whether you need an inline search field that filters a ListView in real-time or a dedicated full-screen search page, the framework offers well-defined patterns implemented in the flutter/flutter repository.
Architectural Approaches for Flutter Search
When implementing search functionality, you must choose between two primary architectures based on your UX requirements.
Inline Search Bar with TextField or SearchBar
Embed a TextField (or the newer SearchBar widget) directly above your ListView. This approach keeps the user on the same screen, providing immediate visual feedback as the list filters. It is ideal for contact lists, settings pages, or any interface where search is a secondary but persistent feature.
Full-Screen Search with SearchDelegate
Use the SearchDelegate API when you need a dedicated search experience. This creates a full-screen route with a custom app bar, handles focus transitions automatically, and provides distinct screens for suggestions and results. The core implementation lives in packages/flutter/lib/src/material/search.dart, where SearchDelegate<T> defines the contract and showSearch creates the route.
Implementing an Inline Functional Search Bar
For real-time filtering without navigation, combine a TextEditingController with a ListView.builder. This mirrors the query-change handling performed internally by SearchDelegate, but gives you direct control over the UI layout.
Complete Implementation Example
import 'package:flutter/material.dart';
class SearchableListPage extends StatefulWidget {
const SearchableListPage({Key? key}) : super(key: key);
@override
State<SearchableListPage> createState() => _SearchableListPageState();
}
class _SearchableListPageState extends State<SearchableListPage> {
// Sample data – replace with your own model.
final List<String> _allItems = List<String>.generate(
200,
(i) => 'Item #${i + 1}',
);
// Holds the current query.
final TextEditingController _controller = TextEditingController();
// The filtered list displayed by the ListView.
late List<String> _filteredItems;
@override
void initState() {
super.initState();
// Initially show all items.
_filteredItems = List.from(_allItems);
// Listen for changes and recompute the filtered list.
_controller.addListener(_filterList);
}
@override
void dispose() {
_controller.removeListener(_filterList);
_controller.dispose();
super.dispose();
}
void _filterList() {
final query = _controller.text.toLowerCase();
setState(() {
if (query.isEmpty) {
// When the query is empty, show the complete list.
_filteredItems = List.from(_allItems);
} else {
// Keep only items that contain the query substring.
_filteredItems = _allItems
.where((item) => item.toLowerCase().contains(query))
.toList();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Searchable List'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Search…',
prefixIcon: const Icon(Icons.search),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _controller.clear(),
)
: null,
border: const OutlineInputBorder(),
),
textInputAction: TextInputAction.search,
),
),
Expanded(
child: _filteredItems.isEmpty
? const Center(child: Text('No results found.'))
: ListView.builder(
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
final item = _filteredItems[index];
return ListTile(
leading: const Icon(Icons.label),
title: Text(item),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Selected: $item')),
);
},
);
},
),
),
],
),
);
}
}
Key Implementation Details
| Step | Explanation |
|---|---|
| Controller + Listener | The TextEditingController notifies _filterList on each keystroke, mirroring the query‑change handling performed internally by SearchDelegate in packages/flutter/lib/src/material/search.dart. |
| Case‑insensitive matching | Converting both query and item to lower‑case ensures user‑friendly search behavior, matching the typical implementation of SearchDelegate.buildSuggestions. |
| Empty query fallback | When the field is cleared, the full data set is restored. This mirrors the "show suggestions again" pattern in SearchDelegate.showSuggestions. |
| Performance tip | For large data sets, consider debouncing the listener with Future.delayed or using ValueListenableBuilder to avoid unnecessary rebuilds. |
Using SearchDelegate for Full-Screen Search
When you need a dedicated search page with built-in animations and focus management, extend SearchDelegate and use showSearch.
SearchDelegate Implementation
class SimpleSearchDelegate extends SearchDelegate<String> {
SimpleSearchDelegate(this.items);
final List<String> items;
@override
Widget buildSuggestions(BuildContext context) {
final suggestions = query.isEmpty
? items
: items.where((e) => e.toLowerCase().contains(query.toLowerCase())).toList();
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (context, index) {
final suggestion = suggestions[index];
return ListTile(
title: Text(suggestion),
onTap: () {
query = suggestion;
showResults(context);
},
);
},
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
@override
List<Widget>? buildActions(BuildContext context) => <Widget>[
if (query.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => query = '',
),
];
@override
Widget? buildLeading(BuildContext context) => IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => close(context, ''),
);
}
Triggering the Search
IconButton(
icon: const Icon(Icons.search),
onPressed: () async {
final result = await showSearch<String>(
context: context,
delegate: SimpleSearchDelegate(myItemList),
);
if (result != null && result.isNotEmpty) {
// Handle the selected result.
}
},
);
The delegate's lifecycle (buildSuggestions, buildResults, close) follows the flow defined in packages/flutter/lib/src/material/search.dart, where showSearch creates a _SearchPageRoute and wires the delegate to the UI.
Key Source Files in the Flutter Repository
Understanding the underlying implementation helps you customize behavior and debug issues.
| File | Role | Relevant Content |
|---|---|---|
packages/flutter/lib/src/material/search.dart |
Core SearchDelegate abstraction, showSearch helper, and the internal _SearchPageRoute that drives the full-screen UI. |
SearchDelegate class definition, query getter/setter, _onQueryChanged method. |
packages/flutter/lib/src/material/search_bar_theme.dart |
Theming infrastructure for the newer SearchBar widget (available from Flutter 3.7+). |
SearchBarThemeData, visual styling properties. |
examples/api/lib/material/search_anchor/search_bar.0.dart |
Official example demonstrating a SearchBar placed inside a page with anchored search suggestions. |
Concrete implementation of inline search patterns. |
dev/integration_tests/flutter_gallery/lib/demo/material/search_demo.dart |
Real-world demo from the Flutter Gallery app showing both inline and full-screen search patterns. | Complex usage of SearchDelegate with custom styling. |
packages/flutter/test/material/search_test.dart |
Unit tests verifying SearchDelegate behavior, query updates, and lifecycle methods. |
Test cases for showSearch, query changes, and result handling. |
Summary
- Inline search provides the fastest UX for filtering a
ListViewwithout navigation. Combine aTextFieldorSearchBarwith aTextEditingControllerlistener to recompute your filtered list on every keystroke. - Full-screen search leverages
SearchDelegateandshowSearchto get a dedicated search page with built-in animations, focus management, and suggestion/results separation. The implementation resides inpackages/flutter/lib/src/material/search.dart. - Performance matters for large datasets. Debounce your controller listener or use
ValueListenableBuilderto minimize rebuilds when implementing inline search. - Theming is consistent across both approaches. Use
SearchBarTheme(orInputDecorationforTextField) to match your app's design system.
Frequently Asked Questions
What is the difference between SearchBar and SearchDelegate?
SearchBar is a widget introduced in Flutter 3.7 that provides a material-styled search field you can place anywhere in your widget tree, typically used for inline search implementations. SearchDelegate is an abstract class that defines a contract for full-screen search experiences, handling query updates, suggestions, and results through methods like buildSuggestions and buildResults. While SearchBar stays embedded in your page, SearchDelegate creates a new route via showSearch.
How do I debounce search input to improve performance?
For large datasets, rebuilds on every keystroke can cause jank. Implement debouncing by wrapping your filter logic in a Timer that resets on each controller notification:
Timer? _debounce;
_controller.addListener(() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
_filterList();
});
});
This ensures _filterList only executes after the user stops typing for 500ms, reducing unnecessary setState calls.
Can I use SearchDelegate with custom animations?
Yes, though SearchDelegate uses the internal _SearchPageRoute defined in packages/flutter/lib/src/material/search.dart for standard material transitions. To customize animations, you would need to create your own search implementation using PageRouteBuilder or modify the delegate's behavior by overriding buildTransitions if you were to implement a custom route. However, the standard showSearch helper provides limited animation customization out-of-the-box, favoring consistency with Material Design guidelines.
Where is the SearchDelegate query property defined in Flutter's source?
The query property is defined in the SearchDelegate class located at packages/flutter/lib/src/material/search.dart. It is implemented as a getter and setter that interacts with an internal _SearchPageRoute to update the search field's text and trigger rebuilds of the suggestions and results views. When you modify query programmatically (for example, when a user taps a suggestion), it invokes the same update pipeline as manual user input.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →