Flutter烘焙食谱大全应用开发教程

项目概述

烘焙作为一门精致的生活艺术,越来越受到人们的喜爱。无论是专业烘焙师还是家庭烘焙爱好者,都需要一个便捷的工具来管理和查找烘焙食谱。本教程将带你开发一个功能完整的烘焙食谱大全应用,帮助用户更好地学习和管理烘焙技能。
运行效果图
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 丰富的食谱分类:涵盖蛋糕、面包、饼干、酥点、甜品、派类等六大烘焙分类
  • 详细的制作指导:包含完整的配料清单、制作步骤和实用小贴士
  • 智能筛选系统:支持按分类、难度、关键词等多维度筛选食谱
  • 收藏管理功能:收藏喜爱的食谱,方便随时查看
  • 购物清单管理:一键添加配料到购物清单,便于采购准备
  • 营养信息展示:提供每份食谱的营养信息参考
  • 用户评价系统:查看其他用户的制作心得和评分

技术栈

  • 框架:Flutter 3.x
  • 开发语言:Dart
  • UI设计:Material Design 3
  • 状态管理:StatefulWidget
  • 数据存储:内存存储(可扩展为本地数据库)

核心功能模块

1. 食谱浏览与搜索

  • 食谱列表展示和详情查看
  • 多维度筛选和关键词搜索
  • 食谱分类浏览
  • 难度等级筛选

2. 食谱详情管理

  • 完整的配料清单展示
  • 详细的制作步骤指导
  • 实用的制作小贴士
  • 营养信息参考

3. 收藏系统

  • 食谱收藏和取消收藏
  • 收藏列表管理
  • 快速访问收藏的食谱

4. 购物清单管理

  • 配料一键添加到购物清单
  • 购物项完成状态管理
  • 手动添加购物项
  • 购物进度跟踪

数据模型设计

BakingRecipe(烘焙食谱)模型

class BakingRecipe {
  final String id;              // 食谱唯一标识
  final String name;            // 食谱名称
  final String categoryId;      // 分类ID
  final String description;     // 食谱描述
  final String imageUrl;        // 食谱图片
  final int prepTime;           // 准备时间
  final int cookTime;           // 烘烤时间
  final int servings;           // 份数
  final String difficulty;      // 难度等级
  final List<Ingredient> ingredients; // 配料清单
  final List<String> instructions;    // 制作步骤
  final List<String> tips;      // 制作小贴士
  final double rating;          // 评分
  final int reviewCount;        // 评价数量
  final List<String> tags;      // 标签
  final String author;          // 作者
  final DateTime createdDate;   // 创建日期
  final bool isFavorite;        // 是否收藏
  final String nutritionInfo;   // 营养信息
}

BakingRecipe模型包含了烘焙食谱的完整信息,通过totalTime计算属性获取总制作时间,categoryName、categoryColor、categoryIcon等属性提供分类相关的显示信息,difficultyColor属性根据难度等级返回对应的颜色标识。

Ingredient(配料)模型

class Ingredient {
  final String name;            // 配料名称
  final String amount;          // 用量
  final String unit;            // 单位
  final String? note;           // 备注信息
}

Ingredient模型定义了烘焙配料的基本信息,displayText属性将配料信息格式化为易读的文本格式。

RecipeCategory(食谱分类)模型

class RecipeCategory {
  final String id;              // 分类唯一标识
  final String name;            // 分类名称
  final IconData icon;          // 分类图标
  final Color color;            // 分类颜色
  final String description;     // 分类描述
}

RecipeCategory模型定义了食谱分类的基本信息,为不同分类提供统一的视觉标识和描述。

ShoppingItem(购物项)模型

class ShoppingItem {
  final String id;              // 购物项唯一标识
  final String name;            // 物品名称
  final String amount;          // 数量
  final String unit;            // 单位
  final bool isCompleted;       // 是否已完成
  final String recipeId;        // 关联食谱ID
  final String recipeName;      // 关联食谱名称
}

ShoppingItem模型管理购物清单中的物品信息,支持完成状态跟踪和食谱关联。

RecipeReview(食谱评价)模型

class RecipeReview {
  final String id;              // 评价唯一标识
  final String recipeId;        // 关联食谱ID
  final String userName;        // 用户名
  final double rating;          // 评分
  final String comment;         // 评价内容
  final DateTime reviewDate;    // 评价日期
  final List<String> photos;    // 评价照片
}

RecipeReview模型存储用户对食谱的评价信息,包括评分、评论和照片等。

项目结构设计

lib/
├── main.dart                 # 应用入口文件
├── models/                   # 数据模型
│   ├── baking_recipe.dart   # 烘焙食谱模型
│   ├── ingredient.dart      # 配料模型
│   ├── recipe_category.dart # 食谱分类模型
│   ├── shopping_item.dart   # 购物项模型
│   └── recipe_review.dart   # 食谱评价模型
├── screens/                  # 页面文件
│   ├── home_screen.dart     # 主页面
│   ├── recipes_screen.dart  # 食谱页面
│   ├── categories_screen.dart # 分类页面
│   ├── favorites_screen.dart # 收藏页面
│   └── shopping_screen.dart # 购物页面
├── widgets/                  # 自定义组件
│   ├── recipe_card.dart     # 食谱卡片
│   ├── category_card.dart   # 分类卡片
│   ├── ingredient_list.dart # 配料列表
│   └── step_list.dart       # 步骤列表
└── services/                 # 业务逻辑
    └── recipe_service.dart   # 食谱数据服务

主界面设计与实现

底部导航栏设计

应用采用四个主要功能模块的底部导航设计:

  1. 食谱:浏览和搜索烘焙食谱
  2. 分类:按分类浏览食谱
  3. 收藏:查看收藏的食谱
  4. 购物:管理购物清单
bottomNavigationBar: NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) {
    setState(() => _selectedIndex = index);
  },
  destinations: const [
    NavigationDestination(icon: Icon(Icons.restaurant_menu), label: '食谱'),
    NavigationDestination(icon: Icon(Icons.category), label: '分类'),
    NavigationDestination(icon: Icon(Icons.favorite), label: '收藏'),
    NavigationDestination(icon: Icon(Icons.shopping_cart), label: '购物'),
  ],
),

应用栏设计

appBar: AppBar(
  title: const Text('烘焙食谱大全'),
  backgroundColor: Colors.brown.withValues(alpha: 0.1),
  actions: [
    IconButton(
      onPressed: () {
        _showSearchDialog();
      },
      icon: const Icon(Icons.search),
    ),
    IconButton(
      onPressed: () {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('设置功能开发中...')),
        );
      },
      icon: const Icon(Icons.settings),
    ),
  ],
),

应用栏采用棕色主题,符合烘焙应用的温馨特征。提供搜索和设置功能,增强用户体验。

食谱浏览页面实现

食谱筛选功能

食谱页面顶部提供多维度筛选功能:

Widget _buildRecipeFilters() {
  return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.brown.withValues(alpha: 0.05),
      border: Border(
          bottom: BorderSide(color: Colors.grey.withValues(alpha: 0.3))),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text('食谱筛选', style: TextStyle(fontWeight: FontWeight.w600)),
        const SizedBox(height: 12),
        // 分类筛选
        SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: [
              FilterChip(
                label: const Text('全部'),
                selected: _selectedCategoryId.isEmpty,
                onSelected: (selected) {
                  setState(() {
                    _selectedCategoryId = '';
                  });
                },
              ),
              const SizedBox(width: 8),
              ..._categories.map((category) {
                return Padding(
                  padding: const EdgeInsets.only(right: 8),
                  child: FilterChip(
                    label: Text(category.name),
                    selected: _selectedCategoryId == category.id,
                    onSelected: (selected) {
                      setState(() {
                        _selectedCategoryId = selected ? category.id : '';
                      });
                    },
                  ),
                );
              }),
            ],
          ),
        ),
        const SizedBox(height: 8),
        // 难度筛选
        SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: [
              FilterChip(
                label: const Text('全部难度'),
                selected: _selectedDifficulty.isEmpty,
                onSelected: (selected) {
                  setState(() {
                    _selectedDifficulty = '';
                  });
                },
              ),
              const SizedBox(width: 8),
              ...['简单', '中等', '困难'].map((difficulty) {
                return Padding(
                  padding: const EdgeInsets.only(right: 8),
                  child: FilterChip(
                    label: Text(difficulty),
                    selected: _selectedDifficulty == difficulty,
                    onSelected: (selected) {
                      setState(() {
                        _selectedDifficulty = selected ? difficulty : '';
                      });
                    },
                  ),
                );
              }),
            ],
          ),
        ),
      ],
    ),
  );
}

筛选功能支持以下维度:

  • 分类筛选:蛋糕、面包、饼干、酥点、甜品、派类
  • 难度筛选:简单、中等、困难
  • 关键词搜索:支持食谱名称和描述的模糊搜索

食谱卡片设计

每个烘焙食谱以卡片形式展示关键信息:

Widget _buildRecipeCard(BakingRecipe recipe) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: InkWell(
      onTap: () => _showRecipeDetail(recipe),
      borderRadius: BorderRadius.circular(12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 食谱图片区域
          Container(
            height: 200,
            width: double.infinity,
            decoration: BoxDecoration(
              color: recipe.categoryColor.withValues(alpha: 0.1),
              borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
            ),
            child: Stack(
              children: [
                Center(
                  child: Icon(
                    recipe.categoryIcon,
                    size: 80,
                    color: recipe.categoryColor.withValues(alpha: 0.3),
                  ),
                ),
                // 分类标签
                Positioned(
                  top: 12,
                  right: 12,
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      color: recipe.categoryColor.withValues(alpha: 0.9),
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Text(
                      recipe.categoryName,
                      style: const TextStyle(
                        fontSize: 12,
                        color: Colors.white,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                ),
                // 难度标签
                Positioned(
                  top: 12,
                  left: 12,
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      color: recipe.difficultyColor.withValues(alpha: 0.9),
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Text(
                      recipe.difficulty,
                      style: const TextStyle(
                        fontSize: 12,
                        color: Colors.white,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                ),
                // 收藏按钮
                Positioned(
                  bottom: 12,
                  right: 12,
                  child: IconButton(
                    onPressed: () {
                      setState(() {
                        // 切换收藏状态
                      });
                    },
                    icon: Icon(
                      recipe.isFavorite ? Icons.favorite : Icons.favorite_border,
                      color: Colors.red,
                    ),
                    style: IconButton.styleFrom(
                      backgroundColor: Colors.white.withValues(alpha: 0.9),
                    ),
                  ),
                ),
              ],
            ),
          ),
          // 食谱信息区域
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  recipe.name,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  recipe.description,
                  style: TextStyle(fontSize: 14, color: Colors.grey[600]),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 12),
                // 时间和份数信息
                Row(
                  children: [
                    Icon(Icons.access_time, size: 16, color: Colors.grey[600]),
                    const SizedBox(width: 4),
                    Text(
                      '${recipe.totalTime}分钟',
                      style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                    ),
                    const SizedBox(width: 16),
                    Icon(Icons.people, size: 16, color: Colors.grey[600]),
                    const SizedBox(width: 4),
                    Text(
                      '${recipe.servings}人份',
                      style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                    ),
                    const Spacer(),
                    // 评分信息
                    Row(
                      children: [
                        Icon(Icons.star, color: Colors.orange, size: 16),
                        const SizedBox(width: 4),
                        Text(
                          recipe.rating.toStringAsFixed(1),
                          style: const TextStyle(
                            fontSize: 12,
                            fontWeight: FontWeight.bold,
                            color: Colors.orange,
                          ),
                        ),
                        const SizedBox(width: 4),
                        Text(
                          '(${recipe.reviewCount})',
                          style: TextStyle(fontSize: 12, color: Colors.grey[500]),
                        ),
                      ],
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                Text(
                  '作者:${recipe.author}',
                  style: TextStyle(fontSize: 12, color: Colors.grey[500]),
                ),
                // 标签展示
                if (recipe.tags.isNotEmpty) ...[
                  const SizedBox(height: 8),
                  Wrap(
                    spacing: 4,
                    children: recipe.tags.map((tag) {
                      return Chip(
                        label: Text(tag, style: const TextStyle(fontSize: 10)),
                        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                      );
                    }).toList(),
                  ),
                ],
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

食谱卡片设计特点:

  • 视觉层次:使用分类颜色和图标创建视觉识别
  • 状态标识:显示难度等级、收藏状态等重要信息
  • 关键信息:突出显示制作时间、份数、评分等核心数据
  • 标签系统:以标签形式展示食谱特色
  • 交互设计:支持收藏切换和详情查看

食谱分类页面

分类网格展示

分类页面以网格形式展示所有烘焙分类:

Widget _buildCategoriesPage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '烘焙分类',
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Text(
          '共${_categories.length}个分类',
          style: TextStyle(fontSize: 14, color: Colors.grey[600]),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: GridView.builder(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 1.0,
              crossAxisSpacing: 16,
              mainAxisSpacing: 16,
            ),
            itemCount: _categories.length,
            itemBuilder: (context, index) {
              return _buildCategoryCard(_categories[index]);
            },
          ),
        ),
      ],
    ),
  );
}

分类卡片设计

每个分类以卡片形式展示详细信息:

Widget _buildCategoryCard(RecipeCategory category) {
  final recipeCount = _recipes.where((recipe) => recipe.categoryId == category.id).length;
  
  return Card(
    child: InkWell(
      onTap: () {
        setState(() {
          _selectedCategoryId = category.id;
          _selectedIndex = 0;
        });
      },
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                color: category.color.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(40),
                border: Border.all(
                    color: category.color.withValues(alpha: 0.3)),
              ),
              child: Icon(
                category.icon,
                color: category.color,
                size: 40,
              ),
            ),
            const SizedBox(height: 12),
            Text(
              category.name,
              style: const TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 4),
            Text(
              '$recipeCount个食谱',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
              ),
            ),
            const SizedBox(height: 8),
            Text(
              category.description,
              style: TextStyle(
                fontSize: 10,
                color: Colors.grey[500],
              ),
              textAlign: TextAlign.center,
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ],
        ),
      ),
    ),
  );
}

分类卡片特点:

  • 视觉识别:每个分类使用独特的颜色和图标
  • 食谱统计:显示该分类下的食谱数量
  • 详细描述:提供分类的详细说明
  • 交互导航:点击分类卡片直接跳转到对应食谱列表

食谱详情对话框

详情展示设计

点击食谱卡片可以查看完整的制作指导:

void _showRecipeDetail(BakingRecipe recipe) {
  showDialog(
    context: context,
    builder: (context) => Dialog(
      child: Container(
        width: double.maxFinite,
        height: MediaQuery.of(context).size.height * 0.8,
        child: Column(
          children: [
            // 标题栏
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: recipe.categoryColor.withValues(alpha: 0.1),
                borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
              ),
              child: Row(
                children: [
                  Icon(recipe.categoryIcon, color: recipe.categoryColor),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      recipe.name,
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  IconButton(
                    onPressed: () => Navigator.pop(context),
                    icon: const Icon(Icons.close),
                  ),
                ],
              ),
            ),
            // 内容区域
            Expanded(
              child: SingleChildScrollView(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // 基本信息
                    Row(
                      children: [
                        Container(
                          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                          decoration: BoxDecoration(
                            color: recipe.difficultyColor.withValues(alpha: 0.1),
                            borderRadius: BorderRadius.circular(12),
                          ),
                          child: Text(
                            recipe.difficulty,
                            style: TextStyle(
                              fontSize: 12,
                              color: recipe.difficultyColor,
                              fontWeight: FontWeight.w500,
                            ),
                          ),
                        ),
                        const SizedBox(width: 8),
                        Text('准备:${recipe.prepTime}分钟'),
                        const SizedBox(width: 8),
                        Text('烘烤:${recipe.cookTime}分钟'),
                        const SizedBox(width: 8),
                        Text('${recipe.servings}人份'),
                      ],
                    ),
                    const SizedBox(height: 16),
                    Text(
                      recipe.description,
                      style: TextStyle(fontSize: 14, color: Colors.grey[600]),
                    ),
                    const SizedBox(height: 16),
                    // 配料清单
                    const Text(
                      '所需配料',
                      style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    ...recipe.ingredients.map((ingredient) => Padding(
                          padding: const EdgeInsets.only(bottom: 4),
                          child: Row(
                            children: [
                              const Icon(Icons.fiber_manual_record, size: 8),
                              const SizedBox(width: 8),
                              Expanded(child: Text(ingredient.displayText)),
                              TextButton(
                                onPressed: () {
                                  _addToShoppingList(ingredient, recipe);
                                },
                                child: const Text('加入购物车'),
                              ),
                            ],
                          ),
                        )),
                    const SizedBox(height: 16),
                    // 制作步骤
                    const Text(
                      '制作步骤',
                      style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    ...recipe.instructions.asMap().entries.map((entry) {
                      final index = entry.key;
                      final instruction = entry.value;
                      return Padding(
                        padding: const EdgeInsets.only(bottom: 12),
                        child: Row(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Container(
                              width: 24,
                              height: 24,
                              decoration: BoxDecoration(
                                color: recipe.categoryColor,
                                borderRadius: BorderRadius.circular(12),
                              ),
                              child: Center(
                                child: Text(
                                  '${index + 1}',
                                  style: const TextStyle(
                                    color: Colors.white,
                                    fontSize: 12,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                              ),
                            ),
                            const SizedBox(width: 12),
                            Expanded(child: Text(instruction)),
                          ],
                        ),
                      );
                    }),
                    // 制作小贴士
                    if (recipe.tips.isNotEmpty) ...[
                      const SizedBox(height: 16),
                      const Text(
                        '制作小贴士',
                        style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 8),
                      ...recipe.tips.map((tip) => Padding(
                            padding: const EdgeInsets.only(bottom: 8),
                            child: Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                const Icon(Icons.lightbulb, size: 16, color: Colors.orange),
                                const SizedBox(width: 8),
                                Expanded(child: Text(tip)),
                              ],
                            ),
                          )),
                    ],
                    const SizedBox(height: 16),
                    // 营养信息
                    Container(
                      width: double.infinity,
                      padding: const EdgeInsets.all(12),
                      decoration: BoxDecoration(
                        color: Colors.green.withValues(alpha: 0.1),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            '营养信息',
                            style: TextStyle(fontWeight: FontWeight.bold),
                          ),
                          const SizedBox(height: 4),
                          Text(recipe.nutritionInfo),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

食谱详情特点:

  • 完整信息展示:包含配料、步骤、小贴士、营养信息等完整内容
  • 步骤编号:制作步骤使用数字编号,清晰易懂
  • 一键购物:配料可以直接添加到购物清单
  • 视觉层次:使用不同的颜色和图标区分不同类型的信息
  • 响应式设计:适配不同屏幕尺寸

收藏管理功能

收藏列表展示

收藏页面显示用户收藏的所有食谱:

Widget _buildFavoritesPage() {
  final favoriteRecipes = _recipes.where((recipe) => recipe.isFavorite).toList();

  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '我的收藏',
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Text(
          '共${favoriteRecipes.length}个收藏',
          style: TextStyle(fontSize: 14, color: Colors.grey[600]),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: favoriteRecipes.isEmpty
              ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.favorite_border, size: 64, color: Colors.grey),
                      SizedBox(height: 16),
                      Text('还没有收藏的食谱', style: TextStyle(color: Colors.grey)),
                      SizedBox(height: 8),
                      Text('去发现一些美味的食谱吧!', style: TextStyle(color: Colors.grey)),
                    ],
                  ),
                )
              : ListView.builder(
                  itemCount: favoriteRecipes.length,
                  itemBuilder: (context, index) {
                    return _buildFavoriteRecipeCard(favoriteRecipes[index]);
                  },
                ),
        ),
      ],
    ),
  );
}

收藏卡片设计

收藏的食谱以简化的卡片形式展示:

Widget _buildFavoriteRecipeCard(BakingRecipe recipe) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: InkWell(
      onTap: () => _showRecipeDetail(recipe),
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                color: recipe.categoryColor.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(8),
                border: Border.all(
                    color: recipe.categoryColor.withValues(alpha: 0.3)),
              ),
              child: Icon(
                recipe.categoryIcon,
                color: recipe.categoryColor,
                size: 40,
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    recipe.name,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    recipe.categoryName,
                    style: TextStyle(
                      fontSize: 12,
                      color: recipe.categoryColor,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Icon(Icons.access_time, size: 14, color: Colors.grey[600]),
                      const SizedBox(width: 4),
                      Text(
                        '${recipe.totalTime}分钟',
                        style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                      ),
                      const SizedBox(width: 16),
                      Icon(Icons.star, color: Colors.orange, size: 14),
                      const SizedBox(width: 4),
                      Text(
                        recipe.rating.toStringAsFixed(1),
                        style: const TextStyle(fontSize: 12, color: Colors.orange),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            IconButton(
              onPressed: () {
                setState(() {
                  // 取消收藏的逻辑
                });
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('已取消收藏')),
                );
              },
              icon: const Icon(Icons.favorite, color: Colors.red),
            ),
          ],
        ),
      ),
    ),
  );
}

收藏功能特点:

  • 快速访问:收藏的食谱可以快速查看和访问
  • 状态管理:支持收藏和取消收藏操作
  • 空状态处理:当没有收藏时显示友好的提示信息
  • 简化展示:收藏列表使用简化的卡片设计,节省空间

购物清单管理

购物清单页面

购物清单页面帮助用户管理烘焙所需的材料:

Widget _buildShoppingPage() {
  final completedItems = _shoppingList.where((item) => item.isCompleted).length;
  final totalItems = _shoppingList.length;

  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            const Text(
              '购物清单',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const Spacer(),
            TextButton.icon(
              onPressed: () {
                _showAddShoppingItemDialog();
              },
              icon: const Icon(Icons.add),
              label: const Text('添加'),
            ),
          ],
        ),
        const SizedBox(height: 8),
        Text(
          '已完成 $completedItems/$totalItems 项',
          style: TextStyle(fontSize: 14, color: Colors.grey[600]),
        ),
        const SizedBox(height: 8),
        LinearProgressIndicator(
          value: totalItems > 0 ? completedItems / totalItems : 0,
          backgroundColor: Colors.grey[300],
          valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: _shoppingList.isEmpty
              ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.shopping_cart, size: 64, color: Colors.grey),
                      SizedBox(height: 16),
                      Text('购物清单为空', style: TextStyle(color: Colors.grey)),
                      SizedBox(height: 8),
                      Text('添加一些烘焙材料吧!', style: TextStyle(color: Colors.grey)),
                    ],
                  ),
                )
              : ListView.builder(
                  itemCount: _shoppingList.length,
                  itemBuilder: (context, index) {
                    return _buildShoppingItemCard(_shoppingList[index]);
                  },
                ),
        ),
      ],
    ),
  );
}

购物项卡片设计

每个购物项以列表项形式展示:

Widget _buildShoppingItemCard(ShoppingItem item) {
  return Card(
    margin: const EdgeInsets.only(bottom: 8),
    child: ListTile(
      leading: Checkbox(
        value: item.isCompleted,
        onChanged: (value) {
          setState(() {
            // 更新完成状态的逻辑
          });
        },
      ),
      title: Text(
        item.displayText,
        style: TextStyle(
          decoration: item.isCompleted ? TextDecoration.lineThrough : null,
          color: item.isCompleted ? Colors.grey : null,
        ),
      ),
      subtitle: Text(
        '用于:${item.recipeName}',
        style: TextStyle(
          fontSize: 12,
          color: Colors.grey[600],
        ),
      ),
      trailing: IconButton(
        onPressed: () {
          setState(() {
            _shoppingList.remove(item);
          });
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('已删除购物项')),
          );
        },
        icon: const Icon(Icons.delete, color: Colors.red),
      ),
    ),
  );
}

添加购物项对话框

用户可以手动添加购物项:

void _showAddShoppingItemDialog() {
  final nameController = TextEditingController();
  final amountController = TextEditingController();
  String selectedUnit = 'g';
  final units = ['g', 'ml', '个', '包', '瓶', '勺', '杯'];

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('添加购物项'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextFormField(
            controller: nameController,
            decoration: const InputDecoration(
              labelText: '材料名称',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                flex: 2,
                child: TextFormField(
                  controller: amountController,
                  decoration: const InputDecoration(
                    labelText: '数量',
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.number,
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: DropdownButtonFormField<String>(
                  value: selectedUnit,
                  decoration: const InputDecoration(
                    labelText: '单位',
                    border: OutlineInputBorder(),
                  ),
                  items: units.map((unit) {
                    return DropdownMenuItem(
                      value: unit,
                      child: Text(unit),
                    );
                  }).toList(),
                  onChanged: (value) {
                    selectedUnit = value!;
                  },
                ),
              ),
            ],
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            if (nameController.text.isNotEmpty && amountController.text.isNotEmpty) {
              setState(() {
                _shoppingList.add(ShoppingItem(
                  id: 'item_${DateTime.now().millisecondsSinceEpoch}',
                  name: nameController.text,
                  amount: amountController.text,
                  unit: selectedUnit,
                  isCompleted: false,
                  recipeId: '',
                  recipeName: '手动添加',
                ));
              });
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('已添加到购物清单')),
              );
            }
          },
          child: const Text('添加'),
        ),
      ],
    ),
  );
}

购物清单功能特点:

  • 进度跟踪:显示购物进度条和完成统计
  • 状态管理:支持购物项的完成状态切换
  • 食谱关联:显示购物项对应的食谱信息
  • 手动添加:支持用户手动添加购物项
  • 一键添加:从食谱详情页面可以直接添加配料到购物清单

数据生成与管理

食谱分类初始化

系统预定义六个主要烘焙分类:

void _initializeCategories() {
  _categories.addAll([
    RecipeCategory(
      id: 'cake',
      name: '蛋糕',
      icon: Icons.cake,
      color: Colors.pink,
      description: '各式蛋糕制作,从简单海绵蛋糕到复杂裱花蛋糕',
    ),
    RecipeCategory(
      id: 'bread',
      name: '面包',
      icon: Icons.bakery_dining,
      color: Colors.brown,
      description: '手工面包制作,包括吐司、法棍、丹麦面包等',
    ),
    RecipeCategory(
      id: 'cookie',
      name: '饼干',
      icon: Icons.cookie,
      color: Colors.orange,
      description: '酥脆可口的各式饼干,适合下午茶时光',
    ),
    RecipeCategory(
      id: 'pastry',
      name: '酥点',
      icon: Icons.local_dining,
      color: Colors.purple,
      description: '精致酥点制作,包括泡芙、马卡龙等',
    ),
    RecipeCategory(
      id: 'dessert',
      name: '甜品',
      icon: Icons.icecream,
      color: Colors.green,
      description: '各种甜品制作,布丁、慕斯、果冻等',
    ),
    RecipeCategory(
      id: 'pie',
      name: '派类',
      icon: Icons.pie_chart,
      color: Colors.blue,
      description: '美味派类制作,水果派、奶油派等',
    ),
  ]);
}

烘焙食谱数据生成

系统自动生成24个不同类型的烘焙食谱:

void _generateRecipes() {
  final recipeNames = [
    '经典海绵蛋糕', '巧克力慕斯蛋糕', '红丝绒蛋糕', '芝士蛋糕',
    '全麦吐司', '法式长棍面包', '丹麦牛角包', '肉桂卷',
    '黄油曲奇', '巧克力饼干', '燕麦饼干', '马卡龙',
    '奶油泡芙', '千层酥', '蛋挞', '司康饼',
    '提拉米苏', '布丁', '果冻', '慕斯杯',
    '苹果派', '柠檬派', '南瓜派', '奶油派',
  ];

  final random = Random();

  for (int i = 0; i < 24; i++) {
    final categoryId = _categories[i % _categories.length].id;
    
    // 生成配料
    final ingredients = _generateIngredients(categoryId, random);
    
    // 生成制作步骤
    final instructions = _generateInstructions(categoryId, random);
    
    // 生成小贴士
    final tips = _generateTips(random);

    _recipes.add(BakingRecipe(
      id: 'recipe_$i',
      name: recipeNames[i],
      categoryId: categoryId,
      description: descriptions[random.nextInt(descriptions.length)],
      imageUrl: 'recipe_${i + 1}.jpg',
      prepTime: 15 + random.nextInt(45),
      cookTime: 20 + random.nextInt(120),
      servings: 4 + random.nextInt(8),
      difficulty: difficulties[random.nextInt(difficulties.length)],
      ingredients: ingredients,
      instructions: instructions,
      tips: tips,
      rating: 3.5 + random.nextDouble() * 1.5,
      reviewCount: random.nextInt(100),
      tags: selectedTags,
      author: authors[random.nextInt(authors.length)],
      createdDate: DateTime.now().subtract(Duration(days: random.nextInt(365))),
      isFavorite: random.nextBool(),
      nutritionInfo: '每份约${200 + random.nextInt(300)}卡路里',
    ));
  }
}

配料生成逻辑

根据不同分类生成相应的配料清单:

List<Ingredient> _generateIngredients(String categoryId, Random random) {
  final baseIngredients = [
    Ingredient(name: '面粉', amount: '200', unit: 'g'),
    Ingredient(name: '鸡蛋', amount: '2', unit: '个'),
    Ingredient(name: '牛奶', amount: '100', unit: 'ml'),
    Ingredient(name: '黄油', amount: '50', unit: 'g'),
    Ingredient(name: '糖', amount: '80', unit: 'g'),
  ];

  final specialIngredients = {
    'cake': [
      Ingredient(name: '泡打粉', amount: '5', unit: 'g'),
      Ingredient(name: '香草精', amount: '几滴', unit: ''),
    ],
    'bread': [
      Ingredient(name: '酵母', amount: '3', unit: 'g'),
      Ingredient(name: '盐', amount: '2', unit: 'g'),
    ],
    'cookie': [
      Ingredient(name: '巧克力豆', amount: '50', unit: 'g'),
      Ingredient(name: '杏仁片', amount: '30', unit: 'g'),
    ],
    // ... 其他分类的特殊配料
  };

  final ingredients = List<Ingredient>.from(baseIngredients);
  if (specialIngredients.containsKey(categoryId)) {
    ingredients.addAll(specialIngredients[categoryId]!);
  }

  return ingredients;
}

搜索功能实现

搜索对话框

应用提供关键词搜索功能:

void _showSearchDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('搜索食谱'),
      content: TextFormField(
        decoration: const InputDecoration(
          labelText: '输入关键词',
          border: OutlineInputBorder(),
          hintText: '食谱名称、描述...',
        ),
        onChanged: (value) {
          setState(() {
            _searchKeyword = value;
          });
        },
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
            setState(() => _selectedIndex = 0);
          },
          child: const Text('搜索'),
        ),
      ],
    ),
  );
}

搜索筛选逻辑

搜索功能支持多维度筛选:

final filteredRecipes = _recipes.where((recipe) {
  // 分类筛选
  if (_selectedCategoryId.isNotEmpty && recipe.categoryId != _selectedCategoryId) {
    return false;
  }
  // 难度筛选
  if (_selectedDifficulty.isNotEmpty && recipe.difficulty != _selectedDifficulty) {
    return false;
  }
  // 关键词搜索
  if (_searchKeyword.isNotEmpty && 
      !recipe.name.toLowerCase().contains(_searchKeyword.toLowerCase()) &&
      !recipe.description.toLowerCase().contains(_searchKeyword.toLowerCase())) {
    return false;
  }
  return true;
}).toList();

搜索功能特点:

  • 多维度筛选:支持分类、难度、关键词的组合筛选
  • 模糊匹配:关键词搜索支持食谱名称和描述的模糊匹配
  • 实时筛选:输入关键词后实时更新食谱列表
  • 状态保持:搜索条件在页面切换时保持不变

用户界面设计原则

Material Design 3 应用

应用全面采用Material Design 3设计规范:

  1. 颜色系统:使用棕色作为主题色,符合烘焙应用的温馨特征
  2. 组件设计:使用最新的Material 3组件,如NavigationBar、FilterChip等
  3. 视觉层次:通过不同的字体大小、颜色深浅建立清晰的信息层次
  4. 交互反馈:所有可点击元素都提供适当的视觉反馈

烘焙主题设计

针对烘焙应用的特殊需求:

  1. 分类色彩化

    • 蛋糕:粉色,温馨甜美
    • 面包:棕色,朴实自然
    • 饼干:橙色,活泼可爱
    • 酥点:紫色,精致优雅
    • 甜品:绿色,清新自然
    • 派类:蓝色,经典稳重
  2. 难度标识

    • 简单:绿色,鼓励尝试
    • 中等:橙色,适度挑战
    • 困难:红色,谨慎选择
  3. 状态可视化

    • 收藏状态:红色心形图标
    • 完成状态:绿色对勾标识
    • 进度显示:线性进度条

响应式布局设计

// 网格布局自适应
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 1.0,
    crossAxisSpacing: 16,
    mainAxisSpacing: 16,
  ),
  itemCount: _categories.length,
  itemBuilder: (context, index) {
    return _buildCategoryCard(_categories[index]);
  },
)

// 弹性布局
Row(
  children: [
    Expanded(child: widget1),
    const SizedBox(width: 16),
    Expanded(child: widget2),
  ],
)

状态管理与数据流

应用状态架构

烘焙食谱应用采用StatefulWidget进行状态管理,通过setState方法实现界面更新。主要状态变量包括:

class _BakingRecipeHomePageState extends State<BakingRecipeHomePage> {
  int _selectedIndex = 0;                    // 当前选中的底部导航索引
  final List<RecipeCategory> _categories = []; // 食谱分类列表
  final List<BakingRecipe> _recipes = [];      // 食谱数据列表
  final List<RecipeReview> _reviews = [];      // 用户评价列表
  final List<ShoppingItem> _shoppingList = []; // 购物清单列表
  String _selectedCategoryId = '';             // 当前选中的分类ID
  String _searchKeyword = '';                  // 搜索关键词
  String _selectedDifficulty = '';             // 选中的难度等级
}

数据初始化流程

应用启动时通过initState方法初始化所有数据:


void initState() {
  super.initState();
  _initializeCategories();    // 初始化分类数据
  _generateRecipes();         // 生成食谱数据
  _generateReviews();         // 生成评价数据
  _generateShoppingList();    // 生成购物清单数据
}

数据初始化采用分步骤的方式,确保数据的完整性和一致性:

  1. 分类初始化:首先创建六个主要烘焙分类
  2. 食谱生成:基于分类生成对应的食谱数据
  3. 评价生成:为食谱生成用户评价信息
  4. 购物清单生成:创建示例购物清单数据

状态更新机制

应用中的状态更新主要通过以下几种方式:

1. 导航状态更新
NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) {
    setState(() => _selectedIndex = index);
  },
  // ...
)
2. 筛选状态更新
FilterChip(
  label: Text(category.name),
  selected: _selectedCategoryId == category.id,
  onSelected: (selected) {
    setState(() {
      _selectedCategoryId = selected ? category.id : '';
    });
  },
)
3. 购物清单状态更新
void _addToShoppingList(Ingredient ingredient, BakingRecipe recipe) {
  final existingItem = _shoppingList.firstWhere(
    (item) => item.name == ingredient.name && item.recipeId == recipe.id,
    orElse: () => ShoppingItem(/* 空对象 */),
  );

  if (existingItem.id.isEmpty) {
    setState(() {
      _shoppingList.add(ShoppingItem(
        id: 'item_${DateTime.now().millisecondsSinceEpoch}',
        name: ingredient.name,
        amount: ingredient.amount,
        unit: ingredient.unit,
        isCompleted: false,
        recipeId: recipe.id,
        recipeName: recipe.name,
      ));
    });
  }
}

数据筛选与搜索逻辑

应用实现了多维度的数据筛选功能:

final filteredRecipes = _recipes.where((recipe) {
  // 分类筛选
  if (_selectedCategoryId.isNotEmpty && 
      recipe.categoryId != _selectedCategoryId) {
    return false;
  }
  
  // 难度筛选
  if (_selectedDifficulty.isNotEmpty && 
      recipe.difficulty != _selectedDifficulty) {
    return false;
  }
  
  // 关键词搜索
  if (_searchKeyword.isNotEmpty && 
      !recipe.name.toLowerCase().contains(_searchKeyword.toLowerCase()) &&
      !recipe.description.toLowerCase().contains(_searchKeyword.toLowerCase())) {
    return false;
  }
  
  return true;
}).toList();

筛选逻辑特点:

  • 链式筛选:多个筛选条件可以同时生效
  • 大小写不敏感:搜索时忽略大小写差异
  • 多字段搜索:支持在食谱名称和描述中搜索
  • 实时更新:筛选条件变化时立即更新结果

性能优化策略

列表渲染优化

1. ListView.builder使用

应用中的所有列表都使用ListView.builder进行懒加载:

ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: filteredRecipes.length,
  itemBuilder: (context, index) {
    return _buildRecipeCard(filteredRecipes[index]);
  },
)

优势:

  • 按需渲染:只渲染可见区域的列表项
  • 内存优化:避免一次性创建所有列表项
  • 滚动性能:提供流畅的滚动体验
2. GridView.builder优化

分类页面使用GridView.builder实现网格布局:

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 1.0,
    crossAxisSpacing: 16,
    mainAxisSpacing: 16,
  ),
  itemCount: _categories.length,
  itemBuilder: (context, index) {
    return _buildCategoryCard(_categories[index]);
  },
)

状态管理优化

1. 局部状态更新

避免不必要的全局状态更新,只更新相关的UI部分:

// 好的做法:只更新相关状态
setState(() {
  _selectedCategoryId = selected ? category.id : '';
});

// 避免:更新整个页面状态
setState(() {
  // 大量无关的状态更新
});
2. 计算属性缓存

在数据模型中使用计算属性避免重复计算:

class BakingRecipe {
  // 缓存计算结果
  int get totalTime => prepTime + cookTime;
  
  String get categoryName {
    // 基于categoryId计算分类名称
    switch (categoryId) {
      case 'cake': return '蛋糕';
      // ...
    }
  }
}

内存管理优化

1. 控制器生命周期管理
class _AddShoppingItemDialogState extends State<AddShoppingItemDialog> {
  late TextEditingController nameController;
  late TextEditingController amountController;

  
  void initState() {
    super.initState();
    nameController = TextEditingController();
    amountController = TextEditingController();
  }

  
  void dispose() {
    nameController.dispose();
    amountController.dispose();
    super.dispose();
  }
}
2. 图片资源优化

虽然当前应用使用图标代替图片,但在实际项目中应注意:

// 使用缓存网络图片
CachedNetworkImage(
  imageUrl: recipe.imageUrl,
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
  memCacheWidth: 300, // 限制内存中的图片尺寸
  memCacheHeight: 200,
)

用户体验优化

1. 加载状态处理
Widget _buildRecipesPage() {
  if (_isLoading) {
    return const Center(child: CircularProgressIndicator());
  }
  
  if (_recipes.isEmpty) {
    return const Center(
      child: Text('暂无食谱数据'),
    );
  }
  
  return _buildRecipesList();
}
2. 错误状态处理
Widget _buildErrorState(String message) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.error_outline, size: 64, color: Colors.grey),
        SizedBox(height: 16),
        Text(message, style: TextStyle(color: Colors.grey)),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: _retryLoadData,
          child: Text('重试'),
        ),
      ],
    ),
  );
}
3. 空状态设计
Widget _buildEmptyState() {
  return const Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.favorite_border, size: 64, color: Colors.grey),
        SizedBox(height: 16),
        Text('还没有收藏的食谱', style: TextStyle(color: Colors.grey)),
        SizedBox(height: 8),
        Text('去发现一些美味的食谱吧!', style: TextStyle(color: Colors.grey)),
      ],
    ),
  );
}

扩展功能建议

数据持久化

1. 本地数据库集成

使用sqflite包实现本地数据存储:

// pubspec.yaml
dependencies:
  sqflite: ^2.3.0
  path: ^1.8.3

// database_helper.dart
class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  factory DatabaseHelper() => _instance;
  DatabaseHelper._internal();

  static Database? _database;

  Future<Database> get database async {
    _database ??= await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    String path = join(await getDatabasesPath(), 'baking_recipes.db');
    return await openDatabase(
      path,
      version: 1,
      onCreate: _createTables,
    );
  }

  Future<void> _createTables(Database db, int version) async {
    await db.execute('''
      CREATE TABLE recipes(
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        category_id TEXT NOT NULL,
        description TEXT,
        prep_time INTEGER,
        cook_time INTEGER,
        servings INTEGER,
        difficulty TEXT,
        rating REAL,
        is_favorite INTEGER DEFAULT 0,
        created_date TEXT
      )
    ''');

    await db.execute('''
      CREATE TABLE ingredients(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        recipe_id TEXT NOT NULL,
        name TEXT NOT NULL,
        amount TEXT NOT NULL,
        unit TEXT NOT NULL,
        note TEXT,
        FOREIGN KEY (recipe_id) REFERENCES recipes (id)
      )
    ''');
  }
}
2. SharedPreferences设置存储
class SettingsService {
  static const String _keyThemeMode = 'theme_mode';
  static const String _keyDefaultDifficulty = 'default_difficulty';
  static const String _keyNotificationEnabled = 'notification_enabled';

  static Future<void> saveThemeMode(ThemeMode mode) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyThemeMode, mode.toString());
  }

  static Future<ThemeMode> getThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    final modeString = prefs.getString(_keyThemeMode);
    return ThemeMode.values.firstWhere(
      (mode) => mode.toString() == modeString,
      orElse: () => ThemeMode.system,
    );
  }
}

网络功能集成

1. 在线食谱同步
class RecipeApiService {
  static const String baseUrl = 'https://api.bakingrecipes.com';
  
  static Future<List<BakingRecipe>> fetchRecipes() async {
    final response = await http.get(
      Uri.parse('$baseUrl/recipes'),
      headers: {'Content-Type': 'application/json'},
    );
    
    if (response.statusCode == 200) {
      final List<dynamic> data = json.decode(response.body);
      return data.map((json) => BakingRecipe.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load recipes');
    }
  }
  
  static Future<void> uploadRecipe(BakingRecipe recipe) async {
    final response = await http.post(
      Uri.parse('$baseUrl/recipes'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode(recipe.toJson()),
    );
    
    if (response.statusCode != 201) {
      throw Exception('Failed to upload recipe');
    }
  }
}
2. 图片上传功能
class ImageUploadService {
  static Future<String> uploadImage(File imageFile) async {
    final request = http.MultipartRequest(
      'POST',
      Uri.parse('https://api.bakingrecipes.com/upload'),
    );
    
    request.files.add(
      await http.MultipartFile.fromPath('image', imageFile.path),
    );
    
    final response = await request.send();
    
    if (response.statusCode == 200) {
      final responseData = await response.stream.bytesToString();
      final data = json.decode(responseData);
      return data['imageUrl'];
    } else {
      throw Exception('Failed to upload image');
    }
  }
}

社交功能

1. 用户评价系统
class ReviewWidget extends StatefulWidget {
  final BakingRecipe recipe;
  
  const ReviewWidget({Key? key, required this.recipe}) : super(key: key);
  
  
  _ReviewWidgetState createState() => _ReviewWidgetState();
}

class _ReviewWidgetState extends State<ReviewWidget> {
  double _rating = 5.0;
  final _commentController = TextEditingController();
  
  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('为这个食谱评分', style: TextStyle(fontWeight: FontWeight.bold)),
        SizedBox(height: 8),
        RatingBar.builder(
          initialRating: _rating,
          minRating: 1,
          direction: Axis.horizontal,
          allowHalfRating: true,
          itemCount: 5,
          itemPadding: EdgeInsets.symmetric(horizontal: 4.0),
          itemBuilder: (context, _) => Icon(
            Icons.star,
            color: Colors.amber,
          ),
          onRatingUpdate: (rating) {
            setState(() {
              _rating = rating;
            });
          },
        ),
        SizedBox(height: 16),
        TextField(
          controller: _commentController,
          decoration: InputDecoration(
            labelText: '分享你的制作心得',
            border: OutlineInputBorder(),
            hintText: '味道如何?制作过程顺利吗?',
          ),
          maxLines: 3,
        ),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: _submitReview,
          child: Text('提交评价'),
        ),
      ],
    );
  }
  
  void _submitReview() {
    final review = RecipeReview(
      id: 'review_${DateTime.now().millisecondsSinceEpoch}',
      recipeId: widget.recipe.id,
      userName: '当前用户', // 从用户系统获取
      rating: _rating,
      comment: _commentController.text,
      reviewDate: DateTime.now(),
      photos: [], // 可以添加照片上传功能
    );
    
    // 保存评价到数据库或发送到服务器
    _saveReview(review);
  }
}
2. 分享功能
class ShareService {
  static Future<void> shareRecipe(BakingRecipe recipe) async {
    final String text = '''
${recipe.name}

${recipe.description}

制作时间:${recipe.totalTime}分钟
难度:${recipe.difficulty}
份数:${recipe.servings}人份

来自烘焙食谱大全应用
    ''';
    
    await Share.share(
      text,
      subject: '分享美味食谱:${recipe.name}',
    );
  }
  
  static Future<void> shareToSocialMedia(BakingRecipe recipe, String platform) async {
    final String url = 'https://bakingapp.com/recipe/${recipe.id}';
    
    switch (platform) {
      case 'wechat':
        // 集成微信SDK
        break;
      case 'weibo':
        // 集成微博SDK
        break;
      case 'qq':
        // 集成QQ SDK
        break;
    }
  }
}

个性化功能

1. 主题定制
class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.system;
  Color _primaryColor = Colors.brown;
  
  ThemeMode get themeMode => _themeMode;
  Color get primaryColor => _primaryColor;
  
  void setThemeMode(ThemeMode mode) {
    _themeMode = mode;
    notifyListeners();
    _saveThemeMode(mode);
  }
  
  void setPrimaryColor(Color color) {
    _primaryColor = color;
    notifyListeners();
    _savePrimaryColor(color);
  }
  
  ThemeData get lightTheme => ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: _primaryColor,
      brightness: Brightness.light,
    ),
    useMaterial3: true,
  );
  
  ThemeData get darkTheme => ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: _primaryColor,
      brightness: Brightness.dark,
    ),
    useMaterial3: true,
  );
}
2. 个人偏好设置
class UserPreferences {
  static const String _keyFavoriteDifficulty = 'favorite_difficulty';
  static const String _keyPreferredCategories = 'preferred_categories';
  static const String _keyNotificationTime = 'notification_time';
  
  static Future<void> setFavoriteDifficulty(String difficulty) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyFavoriteDifficulty, difficulty);
  }
  
  static Future<String?> getFavoriteDifficulty() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_keyFavoriteDifficulty);
  }
  
  static Future<void> setPreferredCategories(List<String> categories) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setStringList(_keyPreferredCategories, categories);
  }
  
  static Future<List<String>> getPreferredCategories() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getStringList(_keyPreferredCategories) ?? [];
  }
}

智能推荐功能

1. 基于历史的推荐算法
class RecommendationService {
  static List<BakingRecipe> getRecommendedRecipes(
    List<BakingRecipe> allRecipes,
    List<String> favoriteCategories,
    String preferredDifficulty,
    List<String> viewHistory,
  ) {
    final recommendations = <BakingRecipe>[];
    
    // 1. 基于收藏分类推荐
    final categoryRecommendations = allRecipes.where((recipe) =>
        favoriteCategories.contains(recipe.categoryId)).toList();
    recommendations.addAll(categoryRecommendations.take(5));
    
    // 2. 基于难度偏好推荐
    final difficultyRecommendations = allRecipes.where((recipe) =>
        recipe.difficulty == preferredDifficulty).toList();
    recommendations.addAll(difficultyRecommendations.take(3));
    
    // 3. 基于评分推荐高分食谱
    final highRatedRecipes = allRecipes.where((recipe) =>
        recipe.rating >= 4.5).toList()
      ..sort((a, b) => b.rating.compareTo(a.rating));
    recommendations.addAll(highRatedRecipes.take(3));
    
    // 去重并返回
    final uniqueRecommendations = recommendations.toSet().toList();
    return uniqueRecommendations.take(10).toList();
  }
  
  static List<BakingRecipe> getSimilarRecipes(
    BakingRecipe targetRecipe,
    List<BakingRecipe> allRecipes,
  ) {
    return allRecipes.where((recipe) =>
        recipe.id != targetRecipe.id &&
        (recipe.categoryId == targetRecipe.categoryId ||
         recipe.difficulty == targetRecipe.difficulty ||
         recipe.tags.any((tag) => targetRecipe.tags.contains(tag)))
    ).take(5).toList();
  }
}
2. 季节性推荐
class SeasonalRecommendationService {
  static List<BakingRecipe> getSeasonalRecipes(
    List<BakingRecipe> allRecipes,
    DateTime currentDate,
  ) {
    final month = currentDate.month;
    final seasonalTags = _getSeasonalTags(month);
    
    return allRecipes.where((recipe) =>
        recipe.tags.any((tag) => seasonalTags.contains(tag))
    ).toList();
  }
  
  static List<String> _getSeasonalTags(int month) {
    switch (month) {
      case 12:
      case 1:
      case 2:
        return ['温暖', '节日', '巧克力', '肉桂'];
      case 3:
      case 4:
      case 5:
        return ['清新', '柠檬', '草莓', '轻盈'];
      case 6:
      case 7:
      case 8:
        return ['清爽', '水果', '免烤', '冰淇淋'];
      case 9:
      case 10:
      case 11:
        return ['温馨', '苹果', '南瓜', '肉桂'];
      default:
        return [];
    }
  }
}

测试策略

单元测试

1. 数据模型测试
// test/models/baking_recipe_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:baking_recipes/models/baking_recipe.dart';

void main() {
  group('BakingRecipe', () {
    test('should calculate total time correctly', () {
      final recipe = BakingRecipe(
        id: 'test_1',
        name: 'Test Cake',
        categoryId: 'cake',
        description: 'A test cake',
        imageUrl: 'test.jpg',
        prepTime: 30,
        cookTime: 45,
        servings: 8,
        difficulty: '简单',
        ingredients: [],
        instructions: [],
        tips: [],
        rating: 4.5,
        reviewCount: 10,
        tags: [],
        author: 'Test Author',
        createdDate: DateTime.now(),
        isFavorite: false,
        nutritionInfo: 'Test nutrition',
      );

      expect(recipe.totalTime, equals(75));
    });

    test('should return correct category name', () {
      final recipe = BakingRecipe(
        id: 'test_1',
        name: 'Test Cake',
        categoryId: 'cake',
        // ... 其他必需参数
      );

      expect(recipe.categoryName, equals('蛋糕'));
    });

    test('should return correct difficulty color', () {
      final easyRecipe = BakingRecipe(
        // ... 参数
        difficulty: '简单',
      );
      
      final hardRecipe = BakingRecipe(
        // ... 参数
        difficulty: '困难',
      );

      expect(easyRecipe.difficultyColor, equals(Colors.green));
      expect(hardRecipe.difficultyColor, equals(Colors.red));
    });
  });
}
2. 业务逻辑测试
// test/services/recommendation_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:baking_recipes/services/recommendation_service.dart';

void main() {
  group('RecommendationService', () {
    late List<BakingRecipe> testRecipes;

    setUp(() {
      testRecipes = [
        // 创建测试用的食谱数据
      ];
    });

    test('should recommend recipes based on favorite categories', () {
      final recommendations = RecommendationService.getRecommendedRecipes(
        testRecipes,
        ['cake', 'cookie'],
        '简单',
        [],
      );

      expect(recommendations.isNotEmpty, true);
      expect(
        recommendations.every((recipe) => 
          ['cake', 'cookie'].contains(recipe.categoryId) ||
          recipe.difficulty == '简单' ||
          recipe.rating >= 4.5
        ),
        true,
      );
    });

    test('should find similar recipes correctly', () {
      final targetRecipe = testRecipes.first;
      final similarRecipes = RecommendationService.getSimilarRecipes(
        targetRecipe,
        testRecipes,
      );

      expect(similarRecipes.contains(targetRecipe), false);
      expect(similarRecipes.length, lessThanOrEqualTo(5));
    });
  });
}

集成测试

1. 页面导航测试
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:baking_recipes/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('App Integration Tests', () {
    testWidgets('should navigate between pages correctly', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 验证初始页面
      expect(find.text('烘焙食谱大全'), findsOneWidget);
      expect(find.byIcon(Icons.restaurant_menu), findsOneWidget);

      // 点击分类页面
      await tester.tap(find.byIcon(Icons.category));
      await tester.pumpAndSettle();
      expect(find.text('烘焙分类'), findsOneWidget);

      // 点击收藏页面
      await tester.tap(find.byIcon(Icons.favorite));
      await tester.pumpAndSettle();
      expect(find.text('我的收藏'), findsOneWidget);

      // 点击购物页面
      await tester.tap(find.byIcon(Icons.shopping_cart));
      await tester.pumpAndSettle();
      expect(find.text('购物清单'), findsOneWidget);
    });

    testWidgets('should filter recipes correctly', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 点击蛋糕分类筛选
      await tester.tap(find.text('蛋糕'));
      await tester.pumpAndSettle();

      // 验证筛选结果
      expect(find.byType(Card), findsWidgets);
    });

    testWidgets('should add item to shopping list', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 点击第一个食谱
      await tester.tap(find.byType(Card).first);
      await tester.pumpAndSettle();

      // 点击添加到购物车按钮
      await tester.tap(find.text('加入购物车').first);
      await tester.pumpAndSettle();

      // 关闭详情对话框
      await tester.tap(find.byIcon(Icons.close));
      await tester.pumpAndSettle();

      // 切换到购物页面
      await tester.tap(find.byIcon(Icons.shopping_cart));
      await tester.pumpAndSettle();

      // 验证购物项已添加
      expect(find.byType(ListTile), findsWidgets);
    });
  });
}
2. 用户交互测试
testWidgets('should handle search functionality', (tester) async {
  app.main();
  await tester.pumpAndSettle();

  // 点击搜索按钮
  await tester.tap(find.byIcon(Icons.search));
  await tester.pumpAndSettle();

  // 输入搜索关键词
  await tester.enterText(find.byType(TextFormField), '蛋糕');
  await tester.pumpAndSettle();

  // 点击搜索
  await tester.tap(find.text('搜索'));
  await tester.pumpAndSettle();

  // 验证搜索结果
  expect(find.byType(Card), findsWidgets);
});

性能测试

1. 滚动性能测试
testWidgets('should scroll smoothly with many items', (tester) async {
  app.main();
  await tester.pumpAndSettle();

  // 获取列表视图
  final listFinder = find.byType(ListView);
  expect(listFinder, findsOneWidget);

  // 执行滚动操作
  await tester.fling(listFinder, const Offset(0, -500), 1000);
  await tester.pumpAndSettle();

  // 验证滚动后的状态
  expect(find.byType(Card), findsWidgets);
});
2. 内存使用测试
testWidgets('should not leak memory during navigation', (tester) async {
  app.main();
  await tester.pumpAndSettle();

  // 记录初始内存使用
  final initialMemory = await tester.binding.defaultBinaryMessenger
      .send('flutter/system', const StandardMethodCodec().encodeMethodCall(
        const MethodCall('System.requestMemoryInfo'),
      ));

  // 执行多次页面切换
  for (int i = 0; i < 10; i++) {
    await tester.tap(find.byIcon(Icons.category));
    await tester.pumpAndSettle();
    await tester.tap(find.byIcon(Icons.restaurant_menu));
    await tester.pumpAndSettle();
  }

  // 检查内存使用是否在合理范围内
  // 这里可以添加具体的内存检查逻辑
});

部署与发布

Android平台部署

1. 应用签名配置
# 生成签名密钥
keytool -genkey -v -keystore ~/baking-recipes-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias baking-recipes

# 在android/app/build.gradle中配置签名
android {
    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}
2. 构建发布版本
# 清理项目
flutter clean

# 获取依赖
flutter pub get

# 构建APK
flutter build apk --release

# 构建App Bundle(推荐用于Google Play)
flutter build appbundle --release
3. 应用图标和启动画面
# pubspec.yaml
dev_dependencies:
  flutter_launcher_icons: ^0.13.1

flutter_icons:
  android: true
  ios: true
  image_path: "assets/icon/app_icon.png"
  adaptive_icon_background: "#FFFFFF"
  adaptive_icon_foreground: "assets/icon/app_icon_foreground.png"

iOS平台部署

1. Xcode项目配置
# 打开iOS项目
open ios/Runner.xcworkspace

# 在Xcode中配置:
# - Bundle Identifier
# - Team签名
# - 部署目标版本
# - 应用图标和启动画面
2. 构建iOS应用
# 构建iOS应用
flutter build ios --release

# 创建IPA文件
flutter build ipa --release

应用商店发布

1. Google Play Console
# 应用信息配置
app_name: "烘焙食谱大全"
package_name: "com.example.baking_recipes"
version_name: "1.0.0"
version_code: 1

# 应用描述
short_description: "专业的烘焙食谱管理应用,让烘焙变得简单有趣"
full_description: |
  烘焙食谱大全是一款专为烘焙爱好者设计的应用,提供丰富的烘焙食谱和便捷的管理功能。
  
  主要功能:
  • 六大分类的精选烘焙食谱
  • 详细的制作步骤和小贴士
  • 智能筛选和搜索功能
  • 收藏管理和购物清单
  • 简洁美观的Material Design界面
  
  无论你是烘焙新手还是资深爱好者,这款应用都能帮助你轻松制作出美味的烘焙作品。
2. App Store Connect
# 应用元数据
app_name: "烘焙食谱大全"
bundle_id: "com.example.bakingrecipes"
primary_category: "美食佳饮"
secondary_category: "生活"

# 关键词
keywords: "烘焙,食谱,蛋糕,面包,饼干,甜品,制作,教程"

# 应用截图要求
# iPhone: 1290x2796, 1284x2778
# iPad: 2048x2732, 2732x2048

持续集成/持续部署 (CI/CD)

1. GitHub Actions配置
# .github/workflows/build.yml
name: Build and Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.16.0'
    - run: flutter pub get
    - run: flutter analyze
    - run: flutter test

  build-android:
    needs: test
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.16.0'
    - run: flutter pub get
    - run: flutter build apk --release
    - uses: actions/upload-artifact@v3
      with:
        name: android-apk
        path: build/app/outputs/flutter-apk/app-release.apk

  build-ios:
    needs: test
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v3
    - uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.16.0'
    - run: flutter pub get
    - run: flutter build ios --release --no-codesign
2. 自动化测试集成
# .github/workflows/test.yml
name: Automated Tests

on:
  schedule:
    - cron: '0 2 * * *'  # 每天凌晨2点运行
  workflow_dispatch:

jobs:
  integration-test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: subosito/flutter-action@v2
    - name: Enable KVM group perms
      run: |
        echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
        sudo udevadm control --reload-rules
        sudo udevadm trigger --name-match=kvm
    - name: Run integration tests
      uses: reactivecircus/android-emulator-runner@v2
      with:
        api-level: 29
        script: flutter test integration_test/

总结

通过本教程,我们成功开发了一个功能完整的Flutter烘焙食谱大全应用。这个项目展示了现代移动应用开发的多个重要方面:

技术成果

  1. 完整的应用架构:从数据模型设计到用户界面实现,构建了一个结构清晰、易于维护的应用架构
  2. Material Design 3应用:充分利用了Flutter的Material Design 3组件,创造了现代化的用户体验
  3. 状态管理实践:通过StatefulWidget和setState实现了高效的状态管理
  4. 性能优化策略:采用了ListView.builder、GridView.builder等优化技术,确保应用的流畅运行

功能特色

  1. 多维度筛选系统:支持分类、难度、关键词等多种筛选方式
  2. 完整的食谱管理:从浏览、收藏到购物清单,提供了完整的烘焙制作流程支持
  3. 用户友好的界面:采用直观的图标、颜色编码和布局设计,提升用户体验
  4. 扩展性设计:预留了数据持久化、网络功能、社交分享等扩展接口

开发经验

  1. 模块化开发:通过合理的文件组织和组件拆分,提高了代码的可读性和可维护性
  2. 测试驱动开发:建立了完整的测试策略,包括单元测试、集成测试和性能测试
  3. 持续集成实践:通过CI/CD流程,确保代码质量和发布效率
  4. 跨平台兼容:一套代码同时支持Android和iOS平台,大大提高了开发效率

学习价值

这个项目不仅是一个实用的烘焙应用,更是学习Flutter开发的优秀案例。通过这个项目,开发者可以掌握:

  • Flutter基础组件的使用
  • 状态管理的最佳实践
  • 用户界面设计原则
  • 应用性能优化技巧
  • 测试和部署流程

未来展望

基于当前的基础架构,这个应用还有很大的扩展空间:

  1. 智能化功能:集成AI推荐算法,提供个性化的食谱推荐
  2. 社区功能:建立用户社区,支持食谱分享和交流
  3. AR/VR集成:利用增强现实技术,提供更直观的制作指导
  4. IoT设备连接:与智能烤箱等设备联动,实现自动化烘焙

烘焙食谱大全应用展示了Flutter在移动应用开发中的强大能力和灵活性。通过合理的架构设计和用户体验优化,我们创造了一个既实用又美观的应用,为烘焙爱好者提供了便捷的数字化工具。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐