在这里插入图片描述

在浏览菜谱时,我们总会遇到一些特别喜欢的,想要保存下来以后再做。今天我们要实现收藏菜谱功能,让用户能够收藏喜欢的菜谱,并方便地查看所有收藏。

收藏功能的设计思路

收藏功能要解决两个问题:如何展示收藏的菜谱,以及如何让用户快速找到想要的菜谱。我选择了网格布局,因为网格能在一屏内展示更多内容,用户可以快速浏览。

每个菜谱卡片显示图片、名称和收藏标记。图片是最重要的,因为人们对图片的记忆比文字更深刻。收藏标记用红色的心形图标,表示这是用户喜欢的菜谱。

网格采用两列布局,这个数字在手机屏幕上刚好合适。一列会显得太空,三列又会让每个卡片太小。两列是一个平衡点,既能展示足够的内容,每个卡片又有足够的空间。

创建无状态组件

收藏页面的内容相对固定,使用 StatelessWidget 就够了。

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('我的收藏')),
      body: GridView.builder(
        padding: EdgeInsets.all(16.w),
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 12.w,
          mainAxisSpacing: 12.h,
          childAspectRatio: 0.8,
        ),
        itemCount: 20,
        itemBuilder: (context, index) => _buildCard(index),
      ),
    );
  }

页面结构很简单,就是一个 AppBar 和一个 GridView。AppBar 只有标题,保持简洁。

GridView.builder 的 padding 设置为 16.w,让网格内容不要紧贴屏幕边缘。gridDelegate 定义了网格的布局规则。

crossAxisCount 设置为 2,表示两列。crossAxisSpacing 和 mainAxisSpacing 都设置为 12,让卡片之间有适当的间距。

childAspectRatio 设置为 0.8,表示宽高比是 0.8:1,也就是卡片比正方形稍微高一些。这样可以容纳图片和文字信息。

itemCount 现在是固定的 20,实际开发中应该根据收藏数量动态设置。

实现菜谱卡片

每个菜谱卡片包含图片、名称和收藏标记。

  Widget _buildCard(int index) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
      ),

卡片使用白色背景和圆角。这里没有设置阴影,因为背景色已经是浅灰色了,白色卡片本身就有足够的对比度。

      child: Column(
        children: [
          Stack(
            children: [
              Container(
                height: 120.h,
                decoration: BoxDecoration(
                  color: Colors.orange.shade100,
                  borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
                ),
                child: Center(child: Icon(Icons.restaurant, size: 50.sp, color: Colors.orange)),
              ),

顶部是菜谱图片。使用 Stack 是因为我们要在图片上叠加收藏标记。图片容器高度 120.h,背景色是橙色的浅色版本。

borderRadius 只设置顶部的圆角,让图片和卡片的圆角对齐。实际开发中应该显示真实的图片,这里用图标代替。

              Positioned(
                top: 8.h,
                right: 8.w,
                child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
              ),
            ],
          ),

收藏标记使用 Positioned 定位在右上角。图标是红色的心形,大小 20.sp。红色表示这是用户喜欢的菜谱,心形是收藏的通用图标。

top 和 right 都设置为 8,让图标不要紧贴边缘。这个位置既醒目,又不会遮挡图片的主要内容。

          Padding(
            padding: EdgeInsets.all(8.w),
            child: Text('收藏菜谱 ${index + 1}', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
          ),
        ],
      ),
    );
  }
}

底部是菜谱名称。padding 设置为 8.w,让文字不要紧贴边缘。文字使用粗体,字号 14.sp。

这里没有显示其他信息,比如时间、难度等,是为了保持卡片简洁。如果信息太多,卡片会显得拥挤。用户主要是通过图片来识别菜谱的。

添加点击交互

点击卡片应该跳转到菜谱详情页:

Widget _buildCard(int index) {
  return GestureDetector(
    onTap: () {
      Get.to(() => RecipeDetailPage(recipeId: 'recipe_$index'));
    },
    child: Container(
      // ...
    ),
  );
}

使用 GestureDetector 包裹整个卡片,点击时跳转到详情页。传递菜谱 ID 作为参数,详情页会根据这个 ID 加载对应的菜谱数据。

在详情页中,用户可以查看完整的菜谱信息,也可以取消收藏。取消收藏后,这个卡片应该从列表中移除。

实现取消收藏

用户可能想取消收藏某些菜谱。可以在卡片上添加一个取消收藏的按钮:

Positioned(
  top: 8.h,
  right: 8.w,
  child: GestureDetector(
    onTap: () {
      _confirmUnfavorite(context, index);
    },
    child: Container(
      padding: EdgeInsets.all(4.w),
      decoration: BoxDecoration(
        color: Colors.white,
        shape: BoxShape.circle,
      ),
      child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
    ),
  ),
)

把收藏图标改成可点击的按钮。点击时弹出确认对话框,避免误操作。图标外面包了一个白色的圆形容器,让它更醒目,也更容易点击。

void _confirmUnfavorite(BuildContext context, int index) {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text('取消收藏'),
        content: Text('确定要取消收藏这道菜谱吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              // 执行取消收藏操作
              Get.snackbar('成功', '已取消收藏');
            },
            child: Text('确定'),
          ),
        ],
      );
    },
  );
}

确认对话框和删除确认类似,但语气更温和。取消收藏不是危险操作,所以按钮不用红色。

添加空状态

如果用户还没有收藏任何菜谱,需要显示空状态:

if (favorites.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey),
        SizedBox(height: 16.h),
        Text('还没有收藏菜谱', style: TextStyle(color: Colors.grey)),
        SizedBox(height: 8.h),
        ElevatedButton(
          onPressed: () => Get.back(),
          child: Text('去发现菜谱'),
        ),
      ],
    ),
  );
}

空状态使用空心的心形图标,表示还没有收藏。文字说明当前状态,按钮引导用户去浏览菜谱。

按钮点击后返回上一页,用户可以继续浏览菜谱并收藏。这种引导能帮助用户快速上手。

添加搜索功能

如果收藏的菜谱很多,可以添加搜索功能:

AppBar(
  title: Text('我的收藏'),
  actions: [
    IconButton(
      icon: Icon(Icons.search),
      onPressed: () {
        showSearch(
          context: context,
          delegate: FavoriteSearchDelegate(),
        );
      },
    ),
  ],
)

搜索按钮放在 AppBar 右侧,点击后打开搜索页面。用户可以输入关键词,快速找到想要的菜谱。

添加分类筛选

收藏的菜谱可能来自不同的分类,可以添加筛选功能:

Container(
  padding: EdgeInsets.all(16.w),
  child: Wrap(
    spacing: 8.w,
    children: ['全部', '川菜', '粤菜', '西餐'].map((category) {
      return ChoiceChip(
        label: Text(category),
        selected: selectedCategory == category,
        onSelected: (selected) {
          setState(() {
            selectedCategory = category;
          });
        },
      );
    }).toList(),
  ),
)

筛选标签放在网格上方,用户可以按分类查看收藏。这需要把 StatelessWidget 改成 StatefulWidget,用一个变量来存储选中的分类。

添加排序功能

用户可能想按不同的方式排序收藏,比如按收藏时间、按名称等:

AppBar(
  title: Text('我的收藏'),
  actions: [
    PopupMenuButton<String>(
      onSelected: (value) {
        // 根据选择的方式排序
      },
      itemBuilder: (context) => [
        PopupMenuItem(value: 'time', child: Text('按收藏时间')),
        PopupMenuItem(value: 'name', child: Text('按名称排序')),
      ],
    ),
  ],
)

PopupMenuButton 会在右上角显示一个菜单按钮,点击后弹出菜单。用户选择排序方式后,重新排序列表并刷新页面。

添加批量操作

如果用户想取消收藏多道菜谱,可以添加批量操作功能:

AppBar(
  title: isSelectionMode ? Text('已选择 $selectedCount 项') : Text('我的收藏'),
  leading: isSelectionMode ? IconButton(
    icon: Icon(Icons.close),
    onPressed: () {
      // 退出选择模式
    },
  ) : null,
  actions: isSelectionMode ? [
    IconButton(
      icon: Icon(Icons.delete),
      onPressed: () {
        // 批量取消收藏
      },
    ),
  ] : [
    IconButton(
      icon: Icon(Icons.select_all),
      onPressed: () {
        // 进入选择模式
      },
    ),
  ],
)

选择模式下,AppBar 显示已选择的数量和批量操作按钮。用户可以勾选多个菜谱,然后一次性取消收藏。

这需要在每个卡片上添加复选框,并用一个 Set 来存储选中的菜谱 ID。

优化网格布局

在不同的屏幕尺寸上,两列布局可能不是最优的。可以根据屏幕宽度动态调整列数:

LayoutBuilder(
  builder: (context, constraints) {
    int crossAxisCount = 2;
    if (constraints.maxWidth > 600) {
      crossAxisCount = 3;
    } else if (constraints.maxWidth > 900) {
      crossAxisCount = 4;
    }
    
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: crossAxisCount,
        // ...
      ),
      // ...
    );
  },
)

LayoutBuilder 可以获取父容器的约束,根据宽度判断应该使用几列。这样在大屏设备上可以显示更多列,充分利用屏幕空间。

总结

收藏菜谱功能使用网格布局,每个菜谱显示图片、名称和收藏标记。用户可以快速浏览所有收藏,也可以点击查看详情或取消收藏。

通过合理的布局和交互设计,我们让收藏功能既美观又实用。用户可以方便地管理自己喜欢的菜谱,随时找到想做的菜。

下一篇文章我们将实现浏览历史记录功能,让用户能够回顾之前看过的菜谱。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐