Menu
CoddyTech

Recipe Book: Favorites

Part of the Lists and Screens section of Coddy's Flutter journey. Lesson 31 of 33.

Some recipes you cook every week. A heart at the end of each row marks a favorite: an empty heart when it is not, a red filled one when it is.

Whether a recipe is a favorite is part of the recipe, so it becomes a field. Unlike the others it changes, so it is not final, and it starts as false:

bool favorite = false;

Data that changes needs setState, so HomeScreen becomes a StatefulWidget. The recipes move out of build into the State class: a list made inside build is made again on every setState, and every heart would empty out.

The heart is an IconButton. Its icon picks the filled or the empty heart with ? :, and pressing it flips the field: recipe.favorite = !recipe.favorite.

challenge icon

Challenge

Hard

Add a favorite heart to every recipe:

  • give Recipe a field bool favorite = false;
  • turn HomeScreen into a StatefulWidget and move the recipes list into its State class
  • give each tile a trailing: IconButton showing Icons.favorite_border, or a red Icons.favorite when the recipe is a favorite. Pressing it flips favorite

Try it yourself

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final List<Recipe> recipes = [
      Recipe(name: 'Pancakes', minutes: 20, ingredients: ['Flour', 'Eggs', 'Milk', 'Butter']),
      Recipe(name: 'Tomato Soup', minutes: 35, ingredients: ['Tomatoes', 'Onion', 'Garlic', 'Olive oil']),
      Recipe(name: 'Guacamole', minutes: 10, ingredients: ['Avocados', 'Lime', 'Onion', 'Salt']),
      Recipe(name: 'Fried Rice', minutes: 25, ingredients: ['Rice', 'Eggs', 'Peas', 'Soy sauce']),
    ];
    return Scaffold(
      appBar: AppBar(title: const Text('Recipe Book')),
      body: ListView.builder(
        itemCount: recipes.length,
        itemBuilder: (context, index) {
          final Recipe recipe = recipes[index];
          return ListTile(
            leading: const Icon(Icons.restaurant_menu),
            title: Text(recipe.name),
            subtitle: Text('${recipe.minutes} min'),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => RecipeScreen(recipe: recipe)),
              );
            },
          );
        },
      ),
    );
  }
}

class RecipeScreen extends StatelessWidget {
  const RecipeScreen({super.key, required this.recipe});

  final Recipe recipe;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(recipe.name)),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text('Ready in ${recipe.minutes} minutes', style: const TextStyle(fontSize: 18)),
          const SizedBox(height: 24),
          const Text('Ingredients', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
          for (String ingredient in recipe.ingredients)
            ListTile(
              leading: const Icon(Icons.check_circle_outline),
              title: Text(ingredient),
            ),
        ],
      ),
    );
  }
}

class Recipe {
  final String name;
  final int minutes;
  final List<String> ingredients;

  Recipe({required this.name, required this.minutes, required this.ingredients});
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Lists and Screens

Practice on your own: Online Flutter compiler