image

07 Feb 2026

9K

35K

Building a Shopping Product Filter Sidebar Widget in Flutter

In modern e-commerce applications, providing users with robust filtering capabilities is crucial for an intuitive and efficient shopping experience. A well-designed product filter sidebar allows users to quickly narrow down vast product catalogs based on criteria like category, price range, brand, and more. This article will guide you through building a dynamic product filter sidebar widget in Flutter, covering the data models, UI components, and state management necessary to create a fully functional filtering system.

Understanding the Core Components

Before diving into the code, let's outline the essential components we'll need:

  • Product Data Model: A simple class to represent our products with properties like ID, name, price, and category.
  • Filter Criteria Model: A class to encapsulate the user's selected filter preferences (e.g., chosen categories, min/max price).
  • Product Listing Screen: The main screen displaying products, which will manage the current filter state and update the displayed products.
  • Filter Sidebar Widget: A dedicated widget (often implemented as a Drawer) containing the UI elements for selecting filter criteria (checkboxes, sliders, buttons).
  • Filtering Logic: The function that takes the raw product list and the filter criteria, returning a filtered list.

1. Product Data Model

First, let's define our Product class and create some dummy data to work with.


class Product {
  final String id;
  final String name;
  final String category;
  final double price;

  Product({
    required this.id,
    required this.name,
    required this.category,
    required this.price,
  });
}

// Dummy product data
List<Product> allProducts = [
  Product(id: 'p1', name: 'T-Shirt', category: 'Apparel', price: 25.0),
  Product(id: 'p2', name: 'Jeans', category: 'Apparel', price: 55.0),
  Product(id: 'p3', name: 'Sneakers', category: 'Footwear', price: 80.0),
  Product(id: 'p4', name: 'Running Shoes', category: 'Footwear', price: 120.0),
  Product(id: 'p5', name: 'Smartwatch', category: 'Electronics', price: 199.0),
  Product(id: 'p6', name: 'Headphones', category: 'Electronics', price: 99.0),
  Product(id: 'p7', name: 'Keyboard', category: 'Electronics', price: 75.0),
  Product(id: 'p8', name: 'Desk Chair', category: 'Furniture', price: 150.0),
  Product(id: 'p9', name: 'Table Lamp', category: 'Furniture', price: 40.0),
  Product(id: 'p10', name: 'Coffee Maker', category: 'Home Goods', price: 60.0),
  Product(id: 'p11', name: 'Toaster', category: 'Home Goods', price: 30.0),
  Product(id: 'p12', name: 'Jacket', category: 'Apparel', price: 110.0),
  Product(id: 'p13', name: 'Boots', category: 'Footwear', price: 95.0),
];

// Extract unique categories for filter options
Set<String> allCategories =
    allProducts.map((product) => product.category).toSet();

2. Filter Criteria Model

To manage the selected filters, we'll create a FilterCriteria class. This makes it easy to pass filter states between widgets.


class FilterCriteria {
  Set<String> selectedCategories;
  RangeValues priceRange;

  FilterCriteria({
    required this.selectedCategories,
    required this.priceRange,
  });

  FilterCriteria copyWith({
    Set<String>? selectedCategories,
    RangeValues? priceRange,
  }) {
    return FilterCriteria(
      selectedCategories: selectedCategories ?? this.selectedCategories,
      priceRange: priceRange ?? this.priceRange,
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;

    return other is FilterCriteria &&
        _setEquals(other.selectedCategories, selectedCategories) &&
        other.priceRange == priceRange;
  }

  @override
  int get hashCode =>
      selectedCategories.hashCode ^ priceRange.hashCode;

  static bool _setEquals<T>(Set<T> a, Set<T> b) {
    if (a.length != b.length) return false;
    for (final T element in a) {
      if (!b.contains(element)) return false;
    }
    return true;
  }
}

3. The Product Listing Page

This will be our main screen. It holds the current filter criteria, displays the filtered products, and provides a way to open the filter sidebar.


import 'package:flutter/material.dart';

// Assuming Product and FilterCriteria classes are defined above

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

  @override
  State<ProductListingScreen> createState() => _ProductListingScreenState();
}

class _ProductListingScreenState extends State<ProductListingScreen> {
  late FilterCriteria _currentFilterCriteria;
  late List<Product> _filteredProducts;

  final double _minPrice = allProducts.map((p) => p.price).reduce((a, b) => a < b ? a : b);
  final double _maxPrice = allProducts.map((p) => p.price).reduce((a, b) => a > b ? a : b);

  @override
  void initState() {
    super.initState();
    _currentFilterCriteria = FilterCriteria(
      selectedCategories: allCategories.toSet(), // Initially all categories selected
      priceRange: RangeValues(_minPrice, _maxPrice), // Full price range
    );
    _applyFilters();
  }

  void _applyFilters() {
    setState(() {
      _filteredProducts = allProducts.where((product) {
        final bool categoryMatches = _currentFilterCriteria.selectedCategories.isEmpty ||
            _currentFilterCriteria.selectedCategories.contains(product.category);

        final bool priceMatches = product.price >= _currentFilterCriteria.priceRange.start &&
            product.price <= _currentFilterCriteria.priceRange.end;

        return categoryMatches && priceMatches;
      }).toList();
    });
  }

  void _openFilterSidebar() async {
    final FilterCriteria? newCriteria = await Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => ProductFilterSidebar(
          initialFilterCriteria: _currentFilterCriteria,
          allCategories: allCategories,
          minPrice: _minPrice,
          maxPrice: _maxPrice,
        ),
      ),
    );

    if (newCriteria != null && newCriteria != _currentFilterCriteria) {
      setState(() {
        _currentFilterCriteria = newCriteria;
      });
      _applyFilters();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product Listing'),
        actions: [
          IconButton(
            icon: const Icon(Icons.filter_list),
            onPressed: _openFilterSidebar,
          ),
        ],
      ),
      body: _filteredProducts.isEmpty
          ? const Center(child: Text('No products found matching your criteria.'))
          : ListView.builder(
              itemCount: _filteredProducts.length,
              itemBuilder: (context, index) {
                final product = _filteredProducts[index];
                return Card(
                  margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          product.name,
                          style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 4),
                        Text('Category: ${product.category}'),
                        const SizedBox(height: 4),
                        Text('\$${product.price.toStringAsFixed(2)}',
                            style: const TextStyle(fontSize: 16, color: Colors.green)),
                      ],
                    ),
                  ),
                );
              },
            ),
    );
  }
}

4. The Filter Sidebar Widget

This widget will be shown as a new route (or could be a Drawer). It allows users to select categories and define a price range. When filters are applied, it returns the new FilterCriteria to the calling screen.


import 'package:flutter/material.dart';

// Assuming Product and FilterCriteria classes are defined above
// Assuming allCategories, _minPrice, _maxPrice are available from the ProductListingScreen context
// For a standalone example, you might pass these directly or get them from a service.

class ProductFilterSidebar extends StatefulWidget {
  final FilterCriteria initialFilterCriteria;
  final Set<String> allCategories;
  final double minPrice;
  final double maxPrice;

  const ProductFilterSidebar({
    super.key,
    required this.initialFilterCriteria,
    required this.allCategories,
    required this.minPrice,
    required this.maxPrice,
  });

  @override
  State<ProductFilterSidebar> createState() => _ProductFilterSidebarState();
}

class _ProductFilterSidebarState extends State<ProductFilterSidebar> {
  late Set<String> _selectedCategories;
  late RangeValues _currentPriceRange;

  @override
  void initState() {
    super.initState();
    _selectedCategories = Set.from(widget.initialFilterCriteria.selectedCategories);
    _currentPriceRange = widget.initialFilterCriteria.priceRange;
  }

  void _toggleCategory(String category, bool? isChecked) {
    setState(() {
      if (isChecked == true) {
        _selectedCategories.add(category);
      } else {
        _selectedCategories.remove(category);
      }
    });
  }

  void _resetFilters() {
    setState(() {
      _selectedCategories = Set.from(widget.allCategories);
      _currentPriceRange = RangeValues(widget.minPrice, widget.maxPrice);
    });
  }

  void _applyFilters() {
    final newCriteria = FilterCriteria(
      selectedCategories: _selectedCategories,
      priceRange: _currentPriceRange,
    );
    Navigator.of(context).pop(newCriteria);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Filter Products'),
        actions: [
          TextButton(
            onPressed: _resetFilters,
            child: const Text('Reset', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView(
              padding: const EdgeInsets.all(16.0),
              children: [
                // Category Filter
                ExpansionTile(
                  title: const Text('Categories'),
                  initiallyExpanded: true,
                  children: widget.allCategories.map((category) {
                    return CheckboxListTile(
                      title: Text(category),
                      value: _selectedCategories.contains(category),
                      onChanged: (bool? value) {
                        _toggleCategory(category, value);
                      },
                    );
                  }).toList(),
                ),
                const Divider(),

                // Price Range Filter
                ListTile(
                  title: const Text('Price Range'),
                  subtitle: Text(
                      '\$${_currentPriceRange.start.toStringAsFixed(2)} - \$${_currentPriceRange.end.toStringAsFixed(2)}'),
                ),
                RangeSlider(
                  values: _currentPriceRange,
                  min: widget.minPrice,
                  max: widget.maxPrice,
                  divisions: ((widget.maxPrice - widget.minPrice) / 10).round().clamp(1, 100).toInt(), // Approx 10 steps
                  labels: RangeLabels(
                    _currentPriceRange.start.toStringAsFixed(2),
                    _currentPriceRange.end.toStringAsFixed(2),
                  ),
                  onChanged: (RangeValues newValues) {
                    setState(() {
                      _currentPriceRange = newValues;
                    });
                  },
                ),
                const Divider(),

                // Add more filter types here (e.g., Brands, Ratings)
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _applyFilters,
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 12),
                ),
                child: const Text('Apply Filters', style: TextStyle(fontSize: 18)),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

5. Putting It All Together (main.dart)

Finally, let's set up our main.dart to run the application.


import 'package:flutter/material.dart';

// Import your Product, FilterCriteria, ProductListingScreen, ProductFilterSidebar files here
// For this example, I'm assuming all code is in one file for simplicity.

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shopping App Filters',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const ProductListingScreen(),
    );
  }
}

// ... (Product, FilterCriteria, ProductListingScreen, ProductFilterSidebar classes go here) ...
// (Alternatively, place them in separate files and import them)

Best Practices and Enhancements

  • State Management: For larger applications, consider more advanced state management solutions like Provider, Riverpod, BLoC, or GetX. This will make managing complex filter logic and sharing state across multiple widgets more scalable.
  • Debouncing: For filters like price ranges, applying filters immediately on every slider change can be performance-intensive. Implement debouncing to wait for a short period after the user stops interacting before applying the filter.
  • More Filter Types: Extend the FilterCriteria and ProductFilterSidebar to include more filter types such as brand, rating, color, size, availability, etc.
  • Accessibility: Ensure all interactive elements are correctly labeled and navigable for users with accessibility needs.
  • Performance Optimization: For extremely large product lists, consider lazy loading, virtualization (using ListView.builder as shown), or even server-side filtering to keep the app responsive.
  • User Experience: Provide clear feedback when filters are applied (e.g., a "Filters Applied" chip), and allow users to easily remove individual filters.

Conclusion

Building a robust product filter sidebar in Flutter involves careful consideration of data modeling, UI design, and state management. By following the steps outlined in this article, you can create an intuitive and functional filtering system that significantly enhances the user experience in your e-commerce applications. Remember to continuously refine your implementation based on user feedback and the specific requirements of your project.

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