Flutter for OpenHarmony 美食烹饪助手 App 实战:食材分类功能实现
本文介绍了一个食材分类功能的实现方案。该功能采用列表布局展示12种常见食材类别,每个卡片包含图标、名称和菜谱数量,方便用户根据现有食材快速查找相关菜谱。文章详细讲解了界面设计思路,包括卡片样式、间距设置、交互设计等,并提供了性能优化建议。该功能解决了用户"冰箱里有某样食材但不知做什么菜"的实际需求,通过清晰的分类和直观的展示方式提升了用户体验。

在烹饪的世界里,食材是一切的基础。有时候我们冰箱里有什么食材,就想找相关的菜谱来做。今天我们要实现食材分类功能,让用户能够根据手头的食材快速找到合适的菜谱。
食材分类的使用场景
食材分类和菜系分类虽然都是分类功能,但使用场景完全不同。菜系分类是按照烹饪风格来组织,而食材分类是按照原料来组织。
想象一下这个场景:你打开冰箱,发现有一块鸡肉快要过期了,想赶紧做掉。这时候你打开应用,点击"鸡肉"分类,就能看到所有用鸡肉做的菜谱。这就是食材分类的价值所在。
我选择了列表布局而不是网格布局,因为食材分类需要显示更多信息。每个食材不仅要显示名称,还要显示有多少道相关菜谱。列表布局能更好地展示这些信息。
定义食材列表
首先要确定支持哪些食材分类。我选择了12种常见的食材类别,涵盖了日常烹饪的主要原料。
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class IngredientCategoryPage extends StatelessWidget {
const IngredientCategoryPage({super.key});
Widget build(BuildContext context) {
final ingredients = [
'猪肉', '牛肉', '鸡肉', '鱼类', '虾类', '蔬菜',
'豆制品', '蛋类', '面食', '米饭', '海鲜', '菌菇',
];
这个列表包含了肉类、海鲜、蔬菜、主食等各种类别。我没有把分类做得太细,因为太细的分类反而会让用户难以选择。比如把猪肉、牛肉、鸡肉分开,而不是统一叫"肉类",这样用户能更精确地找到想要的菜谱。
食材的顺序也有讲究。肉类放在前面,因为肉类菜谱通常更受欢迎。蔬菜和主食放在中间,海鲜和菌菇放在后面。这个顺序符合大多数人的烹饪习惯。
构建列表布局
食材分类使用 ListView 来展示,每个食材占一行。
return Scaffold(
appBar: AppBar(
title: const Text('食材分类'),
),
body: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: ingredients.length,
itemBuilder: (context, index) {
return _buildIngredientItem(ingredients[index], index);
},
),
);
}
ListView.builder 是构建长列表的标准方式。它只会渲染可见区域的内容,即使有成百上千个食材,也不会有性能问题。
padding 设置为 16.w,让列表内容不要紧贴屏幕边缘。itemCount 使用列表长度,这样以后添加或删除食材时,不需要修改其他代码。
itemBuilder 接收 index 参数,我们把它传递给 _buildIngredientItem 方法。这个 index 可以用来生成不同的菜谱数量,让页面看起来更真实。
设计食材卡片
每个食材卡片包含图标、名称和菜谱数量。
Widget _buildIngredientItem(String name, int index) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
卡片使用白色背景和圆角,这是应用的统一风格。margin 设置为 bottom: 12.h,让卡片之间有适当的间距。padding 设置为 16.w,让内容不要紧贴边缘。
阴影的设置和其他页面保持一致:灰色半透明,模糊半径 5,向下偏移 2。这种轻微的阴影能让卡片有浮起的感觉,但又不会太突兀。
child: Row(
children: [
Container(
width: 50.w,
height: 50.w,
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(Icons.restaurant, color: Colors.orange, size: 25.sp),
),
左侧是一个正方形的图标容器,宽高都是 50.w。背景色使用橙色的浅色版本,和应用的主题色保持一致。圆角设置为 8.r,比卡片的圆角小一些,形成层次感。
图标使用餐厅图标,颜色是橙色。虽然所有食材都用同一个图标,但通过背景色的变化,还是能区分不同的食材。如果以后要优化,可以为不同的食材类别使用不同的图标。
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 4.h),
Text(
'${50 + index * 10} 道菜谱',
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
],
),
),
中间是食材名称和菜谱数量。使用 Expanded 让这部分占据剩余空间,这样右侧的箭头图标就会自动靠右对齐。
食材名称使用粗体,字号 16.sp,这是标题的标准大小。菜谱数量使用灰色,字号 12.sp,表示这是次要信息。
菜谱数量使用公式 50 + index * 10 来计算,这样不同的食材会显示不同的数量。虽然这是模拟数据,但看起来更真实。实际开发中,这个数量应该从数据库查询得到。
Icon(Icons.arrow_forward_ios, size: 16.sp, color: Colors.grey),
],
),
);
}
}
右侧是一个向右的箭头图标,表示可以点击进入。这是移动应用的通用设计语言,用户一看就知道这个卡片可以点击。
箭头使用灰色,大小 16.sp。不要太大,否则会抢了主要内容的风头。也不要太小,否则用户可能注意不到。
添加点击交互
虽然现在卡片还不能点击,但我们要为以后的功能预留接口。可以用 GestureDetector 包裹整个卡片:
Widget _buildIngredientItem(String name, int index) {
return GestureDetector(
onTap: () {
// 跳转到该食材的菜谱列表
Get.to(() => IngredientRecipesPage(ingredient: name));
},
child: Container(
// ...
),
);
}
点击后跳转到该食材的菜谱列表页面,传递食材名称作为参数。列表页面会根据这个参数查询相关的菜谱。
也可以使用 InkWell 代替 GestureDetector,这样点击时会有水波纹效果。但要注意 InkWell 的水波纹需要在 Material 组件下才能显示,所以可能需要调整组件结构。
优化列表性能
虽然现在只有12个食材,但如果以后食材数量增加,就需要考虑性能优化了。
ListView.builder 已经做了基本的优化,只渲染可见区域的内容。但如果每个卡片的构建很复杂,还是会影响滚动流畅度。
可以使用 const 构造函数来减少重建。如果卡片的内容是固定的,就把它声明为 const:
const IngredientCard(
name: '猪肉',
count: 50,
icon: Icons.restaurant,
)
这样 Flutter 就不会在每次重建时都创建新的对象,而是复用已有的对象。这对性能有明显的提升。
另一个优化是使用 AutomaticKeepAliveClientMixin。如果列表项包含复杂的状态,可以让它在滚动出屏幕后保持存活,避免重新构建:
class _IngredientCardState extends State<IngredientCard>
with AutomaticKeepAliveClientMixin {
bool get wantKeepAlive => true;
Widget build(BuildContext context) {
super.build(context);
// ...
}
}
不过对于简单的列表项,这个优化可能是过度的。要根据实际情况来决定是否使用。
添加搜索功能
如果食材很多,用户可能想快速找到特定的食材。可以在 AppBar 添加搜索框:
AppBar(
title: TextField(
decoration: InputDecoration(
hintText: '搜索食材...',
border: InputBorder.none,
prefixIcon: Icon(Icons.search),
),
onChanged: (value) {
// 过滤食材列表
},
),
)
用户输入关键词时,实时过滤食材列表。这需要把 StatelessWidget 改成 StatefulWidget,用一个变量来存储过滤后的列表。
也可以使用 showSearch 打开一个专门的搜索页面,这样不会占用 AppBar 的空间,但需要多一步操作。
添加分组功能
如果食材很多,可以考虑分组显示。比如把肉类、海鲜、蔬菜等分成不同的组:
final ingredientGroups = {
'肉类': ['猪肉', '牛肉', '鸡肉'],
'海鲜': ['鱼类', '虾类', '海鲜'],
'蔬菜': ['蔬菜', '菌菇'],
'其他': ['豆制品', '蛋类', '面食', '米饭'],
};
然后使用 ListView 配合 ExpansionTile 来实现可折叠的分组:
ListView(
children: ingredientGroups.entries.map((entry) {
return ExpansionTile(
title: Text(entry.key),
children: entry.value.map((ingredient) {
return _buildIngredientItem(ingredient, 0);
}).toList(),
);
}).toList(),
)
ExpansionTile 默认是展开的,用户可以点击折叠。这样可以让列表更紧凑,用户可以快速浏览所有分组。
添加统计信息
在页面顶部可以显示一些统计信息,比如总共有多少种食材,多少道菜谱:
Container(
margin: EdgeInsets.all(16.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange.shade400, Colors.orange.shade600],
),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('食材种类', '12'),
_buildStatItem('菜谱总数', '500+'),
],
),
)
这个统计卡片使用渐变背景,和应用的主题色保持一致。两个统计项平均分布,让页面看起来更平衡。
处理空状态
如果搜索没有结果,或者某个分类下没有食材,需要显示空状态:
if (filteredIngredients.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64.sp, color: Colors.grey),
SizedBox(height: 16.h),
Text('没有找到相关食材', style: TextStyle(color: Colors.grey)),
],
),
);
}
空状态要友好,不要让用户觉得是出错了。使用图标和文字说明,让用户知道这是正常的情况。
总结
食材分类页面使用列表布局,每个食材显示图标、名称和菜谱数量。这种设计清晰直观,用户能快速找到自己想要的食材。
通过合理的布局和交互设计,我们让食材分类功能既实用又美观。用户可以根据手头的食材快速找到合适的菜谱,这正是这个功能的价值所在。
下一篇文章我们将实现难度筛选功能,让用户能够根据自己的烹饪水平来选择合适的菜谱。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)