image

20 Dec 2025

9K

35K

Building a Filter Search Widget in Flutter

Introduction

In modern applications, users frequently interact with large datasets. To enhance user experience and efficiency, providing a robust search and filter mechanism is crucial. A filter search widget allows users to quickly narrow down a list of items based on specific criteria, making it easier to find relevant information. This article will guide you through the process of building a dynamic and responsive filter search widget in Flutter.

Core Concepts

Before diving into the implementation, let's understand the key concepts involved:

  • State Management: We'll use Flutter's built-in StatefulWidget and setState to manage the UI's state, specifically the search query and the filtered list.
  • TextEditingController: This controller is essential for managing and listening to changes in a TextField widget, which will serve as our search input.
  • ListView.builder: An efficient way to display a scrollable list of items, especially when the number of items can be large or dynamic.
  • Filtering Logic: The core mechanism to process the original list based on the search query and produce a new, filtered list.
  • Debouncing (Optional but Recommended): A technique to delay the execution of a function until a certain amount of time has passed without any further calls. This prevents excessive widget rebuilds and API calls as the user types, improving performance.

Step-by-Step Implementation

1. Setting Up the Project and Data

First, ensure you have a basic Flutter project set up. We'll create a simple list of strings to demonstrate the filtering functionality.


import 'package:flutter/material.dart';
import 'dart:async'; // For debouncing

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Filter Search Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FilterSearchScreen(),
    );
  }
}

class FilterSearchScreen extends StatefulWidget {
  const FilterSearchScreen({super.key});

  @override
  State createState() => _FilterSearchScreenState();
}

class _FilterSearchScreenState extends State {
  final List<String> _allProducts = [
    'Apple',
    'Banana',
    'Orange',
    'Strawberry',
    'Mango',
    'Pineapple',
    'Grape',
    'Watermelon',
    'Blueberry',
    'Raspberry',
  ];

  List<String> _foundProducts = [];
  TextEditingController _searchController = TextEditingController();
  Timer? _debounce;

  @override
  void initState() {
    _foundProducts = _allProducts; // Initially, show all products
    super.initState();
  }

  @override
  void dispose() {
    _searchController.dispose();
    _debounce?.cancel();
    super.dispose();
  }

  // ... rest of the code
}

2. Implementing the Search Input and UI Structure

We'll add an AppBar with a TextField for the search input and a ListView.builder to display the products.


// ... inside _FilterSearchScreenState

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product Search'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'Search for products...',
                prefixIcon: const Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(10),
                ),
                suffixIcon: _searchController.text.isNotEmpty
                    ? IconButton(
                        icon: const Icon(Icons.clear),
                        onPressed: () {
                          _searchController.clear();
                          _runFilter('');
                        },
                      )
                    : null,
              ),
              onChanged: (value) {
                // We'll add the debouncing logic here
                _onSearchChanged(value);
              },
            ),
            const SizedBox(height: 20),
            Expanded(
              child: _foundProducts.isNotEmpty
                  ? ListView.builder(
                      itemCount: _foundProducts.length,
                      itemBuilder: (context, index) {
                        return Card(
                          key: ValueKey(_foundProducts[index]),
                          elevation: 2,
                          margin: const EdgeInsets.symmetric(vertical: 8),
                          child: ListTile(
                            leading: const Icon(Icons.shopping_bag),
                            title: Text(_foundProducts[index]),
                          ),
                        );
                      },
                    )
                  : const Center(
                      child: Text(
                        'No results found',
                        style: TextStyle(fontSize: 24),
                      ),
                    ),
            ),
          ],
        ),
      ),
    );
  }

  // ... filtering and debouncing methods

3. Implementing the Filtering Logic

The _runFilter method will take the search query and update the _foundProducts list based on whether a product's name contains the query (case-insensitive).


// ... inside _FilterSearchScreenState

  void _runFilter(String enteredKeyword) {
    List<String> results = [];
    if (enteredKeyword.isEmpty) {
      // If the search field is empty, show all products
      results = _allProducts;
    } else {
      results = _allProducts
          .where((product) =>
              product.toLowerCase().contains(enteredKeyword.toLowerCase()))
          .toList();
    }

    // Refresh the UI
    setState(() {
      _foundProducts = results;
    });
  }

  // ... debouncing methods

4. Debouncing the Search Input (Recommended)

To prevent the _runFilter method from being called on every keystroke, which can be inefficient for large lists or when fetching data from an API, we'll implement a simple debouncing mechanism using dart:async.Timer.


// ... inside _FilterSearchScreenState

  void _onSearchChanged(String query) {
    if (_debounce?.isActive ?? false) _debounce!.cancel();
    _debounce = Timer(const Duration(milliseconds: 300), () {
      _runFilter(query);
    });
  }

// ... rest of the class

The _onSearchChanged method will be called when the TextField's onChanged callback fires. It cancels any pending debounce timer and starts a new one. If the user stops typing for 300 milliseconds, the _runFilter method is finally executed.

Complete Code Example

Here's the full code for the FilterSearchScreen:


import 'package:flutter/material.dart';
import 'dart:async'; // For debouncing

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Filter Search Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FilterSearchScreen(),
    );
  }
}

class FilterSearchScreen extends StatefulWidget {
  const FilterSearchScreen({super.key});

  @override
  State createState() => _FilterSearchScreenState();
}

class _FilterSearchScreenState extends State {
  final List<String> _allProducts = [
    'Apple',
    'Banana',
    'Orange',
    'Strawberry',
    'Mango',
    'Pineapple',
    'Grape',
    'Watermelon',
    'Blueberry',
    'Raspberry',
    'Kiwi',
    'Peach',
    'Plum',
    'Cherry',
    'Lemon',
    'Lime',
  ];

  List<String> _foundProducts = [];
  TextEditingController _searchController = TextEditingController();
  Timer? _debounce;

  @override
  void initState() {
    _foundProducts = _allProducts; // Initially, show all products
    super.initState();
  }

  @override
  void dispose() {
    _searchController.dispose();
    _debounce?.cancel();
    super.dispose();
  }

  void _runFilter(String enteredKeyword) {
    List<String> results = [];
    if (enteredKeyword.isEmpty) {
      // If the search field is empty, show all products
      results = _allProducts;
    } else {
      results = _allProducts
          .where((product) =>
              product.toLowerCase().contains(enteredKeyword.toLowerCase()))
          .toList();
    }

    // Refresh the UI
    setState(() {
      _foundProducts = results;
    });
  }

  void _onSearchChanged(String query) {
    if (_debounce?.isActive ?? false) _debounce!.cancel();
    _debounce = Timer(const Duration(milliseconds: 300), () {
      _runFilter(query);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product Search'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'Search for products...',
                prefixIcon: const Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(10),
                ),
                suffixIcon: _searchController.text.isNotEmpty
                    ? IconButton(
                        icon: const Icon(Icons.clear),
                        onPressed: () {
                          _searchController.clear();
                          _runFilter('');
                          // Ensure the text field updates immediately
                          setState(() {});
                        },
                      )
                    : null,
              ),
              onChanged: (value) {
                _onSearchChanged(value);
                // Also update the UI to show/hide clear button
                setState(() {});
              },
            ),
            const SizedBox(height: 20),
            Expanded(
              child: _foundProducts.isNotEmpty
                  ? ListView.builder(
                      itemCount: _foundProducts.length,
                      itemBuilder: (context, index) {
                        return Card(
                          key: ValueKey(_foundProducts[index]),
                          elevation: 2,
                          margin: const EdgeInsets.symmetric(vertical: 8),
                          child: ListTile(
                            leading: const Icon(Icons.shopping_bag),
                            title: Text(_foundProducts[index]),
                          ),
                        );
                      },
                    )
                  : const Center(
                      child: Text(
                        'No results found',
                        style: TextStyle(fontSize: 24),
                      ),
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion

Building a filter search widget in Flutter is a fundamental skill for creating dynamic and user-friendly applications. By combining TextEditingController, state management with setState, and efficient list rendering with ListView.builder, you can create a powerful search experience. Incorporating debouncing further refines the performance, ensuring a smooth interaction even with large datasets. You can extend this basic structure to filter more complex data models, integrate with backend APIs, and add advanced filtering options based on multiple criteria.

Related Articles

Dec 20, 2025

Building a Filter Search Widget in Flutter

Building a Filter Search Widget in Flutter Introduction In modern applications, users frequently interact with large datasets. To enhance user experience and e

Dec 20, 2025

Flutter Performance: Lazy Loading Images Optimizing application

Flutter Performance: Lazy Loading Images Optimizing application performance is crucial for delivering a smooth user experience. In Flutter, especially when dea

Dec 20, 2025

Creating Pie Chart & Bar Chart Widgets in Flutter

Creating Pie Chart & Bar Chart Widgets in Flutter Data visualization is a critical aspect of modern applications, providing users with intuitive insights into