Building a Recipe List Widget with Filter & Search in Flutter

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

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en