image

30 Jan 2026

9K

35K

Building a Recipe List Widget with Filter & Search in Flutter

Creating dynamic lists with filtering and search capabilities is a fundamental requirement for many mobile applications, especially those dealing with catalogs, menus, or, in this case, recipes. A well-implemented recipe list allows users to quickly find desired dishes, significantly enhancing the user experience. This article will guide you through building a Flutter widget that displays a list of recipes, complete with an interactive search bar for efficient filtering.

1. Defining the Recipe Model

First, let's define a Dart class to represent our Recipe. This model will structure the data for each recipe, including its title, description, ingredients, and an image URL.


class Recipe {
  final String id;
  final String title;
  final String description;
  final List<String> ingredients;
  final String imageUrl;
  final String category;

  Recipe({
    required this.id,
    required this.title,
    required this.description,
    required this.ingredients,
    required this.imageUrl,
    required this.category,
  });
}

2. Preparing Dummy Data

For demonstration purposes, we'll create a list of dummy Recipe objects. In a real application, this data would typically come from an API or a local database.


final List<Recipe> _allRecipes = [
  Recipe(
    id: 'r1',
    title: 'Spaghetti Carbonara',
    description: 'A classic Italian pasta dish with eggs, hard cheese, cured pork, and black pepper.',
    ingredients: ['Spaghetti', 'Eggs', 'Pancetta', 'Pecorino Romano', 'Black Pepper'],
    imageUrl: 'https://cdn.pixabay.com/photo/2018/07/18/19/09/spaghetti-3547372_960_720.jpg',
    category: 'Italian',
  ),
  Recipe(
    id: 'r2',
    title: 'Chicken Tikka Masala',
    description: 'Roasted chunks of chicken in a spiced curry sauce. A dish of Indian origin.',
    ingredients: ['Chicken', 'Yogurt', 'Ginger', 'Garlic', 'Tomatoes', 'Cream', 'Spices'],
    imageUrl: 'https://cdn.pixabay.com/photo/2019/02/10/06/20/chicken-tikka-masala-3987012_960_720.jpg',
    category: 'Indian',
  ),
  Recipe(
    id: 'r3',
    title: 'Vegetable Stir-Fry',
    description: 'A quick and healthy Asian dish with mixed vegetables and a savory sauce.',
    ingredients: ['Broccoli', 'Carrots', 'Bell Peppers', 'Soy Sauce', 'Ginger', 'Garlic', 'Noodles'],
    imageUrl: 'https://cdn.pixabay.com/photo/2017/01/17/14/06/stir-fry-1987553_960_720.jpg',
    category: 'Asian',
  ),
  Recipe(
    id: 'r4',
    title: 'Beef Stroganoff',
    description: 'A Russian dish of sautéed pieces of beef served in a sauce with smetana (sour cream).',
    ingredients: ['Beef', 'Mushrooms', 'Onions', 'Sour Cream', 'Mustard', 'Beef Broth'],
    imageUrl: 'https://cdn.pixabay.com/photo/2016/10/26/12/32/beef-stroganoff-1771142_960_720.jpg',
    category: 'Russian',
  ),
  Recipe(
    id: 'r5',
    title: 'Sushi Rolls',
    description: 'Traditional Japanese dish of prepared vinegared rice, usually with some sugar and salt, accompanied by a variety of ingredients.',
    ingredients: ['Sushi Rice', 'Nori', 'Fish', 'Vegetables', 'Soy Sauce', 'Wasabi'],
    imageUrl: 'https://cdn.pixabay.com/photo/2017/01/22/16/09/sushi-200000_960_720.jpg',
    category: 'Japanese',
  ),
  Recipe(
    id: 'r6',
    title: 'Tacos',
    description: 'A traditional Mexican dish consisting of a small hand-sized corn or wheat tortilla topped with a filling.',
    ingredients: ['Tortillas', 'Ground Beef', 'Lettuce', 'Cheese', 'Salsa', 'Onions'],
    imageUrl: 'https://cdn.pixabay.com/photo/2019/08/17/01/01/tacos-4411135_960_720.jpg',
    category: 'Mexican',
  ),
];

3. Building the Recipe Card Widget

To display each recipe attractively, we'll create a reusable RecipeCard widget. This widget will be responsible for the layout and presentation of individual recipe details within the list.


import 'package:flutter/material.dart';

class RecipeCard extends StatelessWidget {
  final Recipe recipe;

  const RecipeCard({Key? key, required this.recipe}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: InkWell(
        onTap: () {
          // Handle tap, e.g., navigate to recipe detail screen
          print('Tapped on ${recipe.title}');
        },
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(12.0),
          child: Row(
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.network(
                  recipe.imageUrl,
                  width: 100,
                  height: 100,
                  fit: BoxFit.cover,
                  errorBuilder: (context, error, stackTrace) => Container(
                    width: 100,
                    height: 100,
                    color: Colors.grey[300],
                    child: Icon(Icons.broken_image, color: Colors.grey[600]),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      recipe.title,
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    const SizedBox(height: 4),
                    Text(
                      recipe.description,
                      style: TextStyle(fontSize: 14, color: Colors.grey[600]),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 6,
                      runSpacing: 4,
                      children: recipe.ingredients
                          .take(3) // Show first 3 ingredients as chips
                          .map((ingredient) => Chip(
                                label: Text(ingredient, style: const TextStyle(fontSize: 12)),
                                visualDensity: VisualDensity.compact,
                              ))
                          .toList(),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

4. Implementing the Recipe List Screen with Search and Filter Logic

The RecipeListScreen will be a StatefulWidget to manage the search state and update the list of displayed recipes dynamically. It will include a search bar in the AppBar that toggles visibility and filters the recipes based on user input.


import 'package:flutter/material.dart';

// (Recipe Model and Dummy Data, and RecipeCard widget definitions go here)
// Make sure Recipe class, _allRecipes list, and RecipeCard widget are accessible.

class RecipeListScreen extends StatefulWidget {
  const RecipeListScreen({Key? key}) : super(key: key);

  @override
  _RecipeListScreenState createState() => _RecipeListScreenState();
}

class _RecipeListScreenState extends State<RecipeListScreen> {
  final TextEditingController _searchController = TextEditingController();
  List<Recipe> _filteredRecipes = [];
  bool _isSearching = false;

  @override
  void initState() {
    super.initState();
    _filteredRecipes = _allRecipes; // Initialize with all recipes
    _searchController.addListener(_onSearchChanged);
  }

  @override
  void dispose() {
    _searchController.removeListener(_onSearchChanged);
    _searchController.dispose();
    super.dispose();
  }

  void _onSearchChanged() {
    _searchRecipes(_searchController.text);
  }

  void _searchRecipes(String query) {
    setState(() {
      if (query.isEmpty) {
        _filteredRecipes = _allRecipes;
      } else {
        _filteredRecipes = _allRecipes.where((recipe) {
          final lowerCaseQuery = query.toLowerCase();
          return recipe.title.toLowerCase().contains(lowerCaseQuery) ||
                 recipe.description.toLowerCase().contains(lowerCaseQuery) ||
                 recipe.ingredients.any((ingredient) => ingredient.toLowerCase().contains(lowerCaseQuery)) ||
                 recipe.category.toLowerCase().contains(lowerCaseQuery);
        }).toList();
      }
    });
  }

  void _toggleSearch() {
    setState(() {
      _isSearching = !_isSearching;
      if (!_isSearching) {
        _searchController.clear();
        _searchRecipes(''); // Reset filter when search is closed
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: _isSearching
            ? TextField(
                controller: _searchController,
                autofocus: true,
                style: const TextStyle(color: Colors.white),
                cursorColor: Colors.white,
                decoration: const InputDecoration(
                  hintText: 'Search recipes...',
                  hintStyle: TextStyle(color: Colors.white70),
                  border: InputBorder.none,
                  prefixIcon: Icon(Icons.search, color: Colors.white70),
                  suffixIcon: Icon(Icons.clear, color: Colors.white70),
                ),
                onChanged: _searchRecipes,
                onSubmitted: _searchRecipes,
              )
            : const Text('Recipe List'),
        actions: [
          IconButton(
            icon: Icon(_isSearching ? Icons.close : Icons.search),
            onPressed: _toggleSearch,
          ),
        ],
      ),
      body: _filteredRecipes.isEmpty
          ? const Center(
              child: Text(
                'No recipes found.',
                style: TextStyle(fontSize: 18, color: Colors.grey),
              ),
            )
          : ListView.builder(
              itemCount: _filteredRecipes.length,
              itemBuilder: (context, index) {
                return RecipeCard(recipe: _filteredRecipes[index]);
              },
            ),
    );
  }
}

5. Running the Application

Finally, to see our recipe list in action, we'll set up the main MyApp widget that runs the RecipeListScreen.


import 'package:flutter/material.dart';

// Ensure the Recipe model, _allRecipes list, RecipeCard, and RecipeListScreen
// are all defined or imported in the same file or accessible scope.

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Recipe App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.deepPurple, // Custom AppBar color
          foregroundColor: Colors.white, // Text and icon color
        ),
      ),
      home: const RecipeListScreen(),
    );
  }
}

Conclusion

By following these steps, you've successfully created a Flutter recipe list widget that is both functional and user-friendly. The implementation includes a clear data model, a reusable UI component for individual recipes, and an effective search mechanism that filters the list in real-time. This foundational structure can be easily extended with more complex filtering options (e.g., by category or dietary restrictions), sorting capabilities, and integration with backend services to fetch real-world data, providing a robust starting point for any recipe-centric application.

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