image

29 Dec 2025

9K

35K

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 a ChangeNotifier and rebuilds its dependents when the notifier calls notifyListeners().
  • Consumer: A widget that rebuilds itself whenever the ChangeNotifier it 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, the onChanged callback is triggered. Inside this callback, we call Provider.of<SearchProvider>(context, listen: false).search(query). We set listen: false because we only want to call a method on the provider, not rebuild the TextField itself when the provider changes.
  • Consumer<SearchProvider>: This widget is specifically designed to rebuild its builder method whenever the SearchProvider calls notifyListeners(). It efficiently limits rebuilds to only the list of results, not the entire screen.
  • searchProvider.searchResults: Inside the Consumer, we access the current list of search results directly from the provider instance passed to the builder.
  • 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 each Product from searchResults to a ListTile.

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 Timer to 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 search method in SearchProvider would become async, 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.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is