Building a Search Result List Widget with Provider in Flutter
Creating interactive user interfaces that respond to user input is a common requirement in mobile application development. A search feature, where a user types a query and sees a dynamic list of results, is a prime example. In Flutter, managing the state for such a feature efficiently and maintainably can be achieved using state management solutions like the provider package. This article will guide you through building a search result list widget using provider, focusing on clean architecture and reactive UI updates.
Why Provider for Search Results?
The provider package offers a robust and easy-to-understand way to manage application state. For a search feature, it allows us to:
- Separate the search logic and data from the UI widgets.
- Efficiently rebuild only the necessary parts of the UI when search results change.
- Easily share the search state across different widgets in the widget tree.
- Improve testability and maintainability of the codebase.
Prerequisites
Before you begin, ensure you have a basic understanding of:
- Flutter fundamentals (Widgets, State, Stateless vs. Stateful).
- Dart programming language.
- Basic concepts of state management in Flutter.
Also, add the provider package to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
provider: ^6.0.5 # Use the latest version
Core Concepts
We'll primarily use these components from the provider package:
ChangeNotifier: A simple class that can notify its listeners about changes. Our search model will extend this.ChangeNotifierProvider: A provider that listens to aChangeNotifierand rebuilds its dependents when the notifier callsnotifyListeners().Consumer: A widget that rebuilds itself whenever theChangeNotifierit depends on notifies of changes.
Step-by-Step Implementation
1. Define the Data Model
First, let's create a simple data model for our search results. For demonstration, we'll use a Product class.
// lib/models/product.dart
class Product {
final String id;
final String name;
final String category;
Product({required this.id, required this.name, required this.category});
@override
String toString() {
return 'Product{id: $id, name: $name, category: $category}';
}
}
2. Create the Search Provider (ChangeNotifier)
This class will hold the search logic, the original list of items, the current search query, and the filtered results. It extends ChangeNotifier to notify listeners of state changes.
// lib/providers/search_provider.dart
import 'package:flutter/material.dart';
import '../models/product.dart';
class SearchProvider extends ChangeNotifier {
final List<Product> _allProducts = [
Product(id: 'p1', name: 'Laptop', category: 'Electronics'),
Product(id: 'p2', name: 'Mouse', category: 'Electronics'),
Product(id: 'p3', name: 'Keyboard', category: 'Electronics'),
Product(id: 'p4', name: 'Monitor', category: 'Electronics'),
Product(id: 'p5', name: 'Desk Chair', category: 'Furniture'),
Product(id: 'p6', name: 'Table Lamp', category: 'Furniture'),
Product(id: 'p7', name: 'Smartphone', category: 'Electronics'),
Product(id: 'p8', name: 'Coffee Mug', category: 'Kitchenware'),
Product(id: 'p9', name: 'Notebook', category: 'Stationery'),
Product(id: 'p10', name: 'Pen Set', category: 'Stationery'),
];
List<Product> _searchResults = [];
String _currentQuery = '';
SearchProvider() {
// Initialize search results with all products
_searchResults = _allProducts;
}
List<Product> get searchResults => _searchResults;
String get currentQuery => _currentQuery;
void search(String query) {
_currentQuery = query;
if (query.isEmpty) {
_searchResults = _allProducts;
} else {
_searchResults = _allProducts.where((product) {
return product.name.toLowerCase().contains(query.toLowerCase()) ||
product.category.toLowerCase().contains(query.toLowerCase());
}).toList();
}
notifyListeners(); // Notify widgets that depend on this provider to rebuild
}
}
3. Set Up the Main Application Widget
To make our SearchProvider available to the widgets in our application, we need to wrap our MaterialApp or a parent widget with ChangeNotifierProvider.
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:search_app/providers/search_provider.dart';
import 'package:search_app/screens/search_screen.dart'; // We will create this next
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => SearchProvider(),
child: MaterialApp(
title: 'Search App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const SearchScreen(),
),
);
}
}
4. Build the Search Screen Widget
This screen will contain the search input field and the search result list.
// lib/screens/search_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/search_provider.dart';
import '../models/product.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final TextEditingController _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Product Search'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
labelText: 'Search products...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search),
),
onChanged: (query) {
// Call the search method from the provider
Provider.of<SearchProvider>(context, listen: false).search(query);
},
),
),
Expanded(
child: Consumer<SearchProvider>(
builder: (context, searchProvider, child) {
final List<Product> results = searchProvider.searchResults;
if (results.isEmpty) {
return Center(
child: Text(
searchProvider.currentQuery.isEmpty
? 'Start typing to search'
: 'No results found for "${searchProvider.currentQuery}"',
style: const TextStyle(fontSize: 16, color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
final product = results[index];
return ListTile(
leading: const Icon(Icons.shopping_bag),
title: Text(product.name),
subtitle: Text(product.category),
onTap: () {
// Handle tap, e.g., navigate to product detail
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped on ${product.name}')),
);
},
);
},
);
},
),
),
],
),
);
}
}
Explanation of Key Parts
TextField.onChanged: Every time the user types, theonChangedcallback is triggered. Inside this callback, we callProvider.of<SearchProvider>(context, listen: false).search(query). We setlisten: falsebecause we only want to call a method on the provider, not rebuild theTextFielditself when the provider changes.Consumer<SearchProvider>: This widget is specifically designed to rebuild itsbuildermethod whenever theSearchProvidercallsnotifyListeners(). It efficiently limits rebuilds to only the list of results, not the entire screen.searchProvider.searchResults: Inside theConsumer, we access the current list of search results directly from the provider instance passed to thebuilder.ListView.builder: This is an efficient way to display a potentially long list of items, building only those that are visible on screen. It maps eachProductfromsearchResultsto aListTile.
Further Enhancements (Optional)
- Debouncing Search: To prevent excessive calls to the search function (especially for API calls), you can implement a debounce mechanism using a
Timerto wait for a short pause in user typing before triggering the search. - Asynchronous Search: If your search involves fetching data from a backend API, the
searchmethod inSearchProviderwould becomeasync, and you might want to show a loading indicator. - Error Handling: Implement logic to display error messages if the search operation fails.
- More Complex Filtering: Add options for filtering by category, price range, etc.
Conclusion
You have successfully built a dynamic search result list widget in Flutter using the provider package. By separating your application's state and logic into a ChangeNotifier and leveraging ChangeNotifierProvider and Consumer, you've created a maintainable, efficient, and reactive user interface. This pattern can be extended to manage various other aspects of your application's state, promoting clean code and a better developer experience.