Flutter for OpenHarmony 美食烹饪助手 App 实战:首页主界面实现
摘要 本文介绍了Flutter烹饪应用首页的实现过程。首页采用分层设计,包含横幅、快速入口和多个内容区块。使用StatelessWidget构建页面框架,通过GetX管理路由跳转。主要实现包括: 顶部横幅使用渐变背景和醒目图标吸引用户注意 四个快速入口提供常用功能直达路径 多个内容区块展示推荐、热门菜谱等信息 采用响应式设计确保不同设备显示效果一致 使用代码拆分提高可维护性 首页设计注重用户体验,

完成了底部导航栏的搭建,现在我们要实现应用的门面——首页。首页是用户打开应用后第一眼看到的界面,它的设计直接影响用户对应用的第一印象。一个好的首页不仅要美观,还要能快速引导用户找到他们想要的内容。
首页的设计思路
在设计首页时,我考虑了几个关键点。首先是信息层次,最重要的内容要放在最显眼的位置。其次是操作便捷性,用户常用的功能要能快速访问。最后是视觉吸引力,要用合适的配色和布局吸引用户继续探索。
基于这些考虑,我把首页分成了几个区域。顶部是一个醒目的横幅,展示应用的主题。接下来是四个快速入口,让用户能直达常用功能。然后是几个内容区块,分别展示今日推荐、热门菜谱、菜系分类等内容。
这种布局方式在很多应用中都能看到,因为它确实有效。用户不需要学习就能理解界面的结构,这就是好设计的标志。
搭建页面框架
首页使用 StatelessWidget 就够了,因为它不需要维护复杂的状态。所有的数据都会从外部传入或者从数据库读取。
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'recommended_recipes_page.dart';
import 'popular_recipes_page.dart';
import 'daily_recommendation_page.dart';
import 'cuisine_category_page.dart';
import 'ingredient_category_page.dart';
import 'difficulty_filter_page.dart';
class CookingHomePage extends StatelessWidget {
const CookingHomePage({super.key});
导入语句中包含了几个子页面,这些是用户点击"更多"按钮后会跳转到的页面。使用 GetX 的路由管理,页面跳转会变得很简单。
flutter_screenutil 的导入是为了使用响应式尺寸单位。所有的宽度、高度、字体大小都会使用 .w、.h、.sp 这些扩展方法,确保在不同屏幕上显示效果一致。
构建页面结构
首页的整体结构使用 Scaffold 包裹,这是 Flutter 中最常用的页面容器。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('美食烹饪助手'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
Get.snackbar('提示', '请前往菜谱库进行搜索');
},
),
],
),
AppBar 显示应用的标题,右侧放了一个搜索图标。点击搜索图标会提示用户去菜谱库进行搜索,因为那里有更完整的搜索功能。这种设计既保持了首页的简洁,又给用户提供了明确的指引。
Get.snackbar 是 GetX 提供的一个便捷方法,可以快速显示一个提示消息。相比 ScaffoldMessenger,它的使用更简单,不需要传递 context。
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBanner(),
SizedBox(height: 20.h),
_buildQuickEntry(),
SizedBox(height: 20.h),
_buildSection('今日推荐', Icons.star, () {
Get.to(() => const DailyRecommendationPage());
}),
_buildSection('热门菜谱', Icons.local_fire_department, () {
Get.to(() => const PopularRecipesPage());
}),
_buildSection('菜系分类', Icons.restaurant_menu, () {
Get.to(() => const CuisineCategoryPage());
}),
_buildSection('食材分类', Icons.shopping_basket, () {
Get.to(() => const IngredientCategoryPage());
}),
SizedBox(height: 20.h),
],
),
),
);
}
body 使用 SingleChildScrollView 包裹,这样当内容超过屏幕高度时,用户可以滚动查看。Column 的 crossAxisAlignment 设置为 start,让所有子组件左对齐。
各个组件之间用 SizedBox 分隔,高度使用 20.h,这样在不同屏幕上间距都是合适的。把页面拆分成多个小方法,每个方法负责构建一个区域,这样代码结构清晰,也便于维护。
实现横幅区域
横幅是首页最显眼的部分,我们用渐变色背景配合图标和文字来吸引用户注意。
Widget _buildBanner() {
return Container(
height: 180.h,
margin: EdgeInsets.all(16.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
gradient: const LinearGradient(
colors: [Colors.orange, Colors.deepOrange],
),
),
Container 的高度设置为 180.h,这个高度既能容纳内容,又不会占用太多屏幕空间。margin 设置为 16.w,让横幅和屏幕边缘保持一定距离。
decoration 使用 BoxDecoration 来设置样式。borderRadius 让容器有圆角,看起来更柔和。gradient 使用线性渐变,从橙色渐变到深橙色,这种渐变效果能让横幅更有层次感。
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.restaurant, size: 60.sp, color: Colors.white),
SizedBox(height: 10.h),
Text(
'发现美味,享受烹饪',
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
内容使用 Center 和 Column 组合,让图标和文字垂直居中排列。restaurant 图标大小设置为 60.sp,颜色为白色,在橙色背景上很醒目。
文字使用白色粗体,字号 20.sp。这句"发现美味,享受烹饪"简洁地传达了应用的核心价值。在设计文案时,简短有力比长篇大论更有效。
构建快速入口
快速入口让用户能直达常用功能,这是提升用户体验的重要设计。
Widget _buildQuickEntry() {
final entries = [
{'icon': Icons.recommend, 'title': '推荐菜谱', 'page': const RecommendedRecipesPage()},
{'icon': Icons.whatshot, 'title': '热门', 'page': const PopularRecipesPage()},
{'icon': Icons.star, 'title': '今日推荐', 'page': const DailyRecommendationPage()},
{'icon': Icons.filter_alt, 'title': '难度筛选', 'page': const DifficultyFilterPage()},
];
我定义了一个列表来存储入口信息,每个入口包含图标、标题和目标页面。使用 Map 来组织数据,虽然不如定义一个类那么严谨,但对于这种简单的场景已经够用了。
这四个入口覆盖了用户最常用的功能。推荐菜谱和热门菜谱帮助用户发现新内容,今日推荐提供每日更新的精选,难度筛选让用户能根据自己的水平选择合适的菜谱。
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: entries.map((entry) {
return GestureDetector(
onTap: () => Get.to(() => entry['page'] as Widget),
child: Column(
children: [
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
entry['icon'] as IconData,
color: Colors.orange,
size: 30.sp,
),
),
SizedBox(height: 8.h),
Text(
entry['title'] as String,
style: TextStyle(fontSize: 12.sp),
),
],
),
);
}).toList(),
),
);
}
Row 的 mainAxisAlignment 设置为 spaceAround,让四个入口均匀分布。使用 map 方法遍历列表,为每个入口创建一个组件。
GestureDetector 包裹整个入口,让它可以响应点击。点击时使用 Get.to 跳转到对应的页面。这里需要把 page 转换为 Widget 类型,因为 Map 的值类型是 dynamic。
每个入口是一个垂直排列的图标和文字。图标放在一个圆角矩形容器中,背景色使用浅橙色,和主题色呼应。图标本身使用深橙色,形成对比。
实现内容区块
内容区块是首页的主体部分,展示各种菜谱内容。
Widget _buildSection(String title, IconData icon, VoidCallback onTap) {
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, color: Colors.orange),
SizedBox(width: 8.w),
Text(
title,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
],
),
TextButton(
onPressed: onTap,
child: const Text('更多'),
),
],
),
),
每个区块的标题行包含一个图标、标题文字和"更多"按钮。图标使用橙色,和应用主题保持一致。标题使用粗体,字号 18.sp,比正文大一些,形成层次感。
"更多"按钮使用 TextButton,点击后会跳转到对应的列表页面。这种设计让用户既能在首页快速浏览,又能深入查看更多内容。
SizedBox(
height: 200.h,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 16.w),
itemCount: 5,
itemBuilder: (context, index) {
return _buildRecipeCard(index);
},
),
),
],
);
}
内容列表使用 ListView.builder 横向滚动。高度固定为 200.h,这样可以显示完整的卡片。scrollDirection 设置为 horizontal,让列表横向排列。
itemCount 设置为 5,表示每个区块显示 5 个菜谱。实际应用中,这个数字应该根据数据动态确定。使用 builder 模式可以实现懒加载,只有可见的卡片才会被创建。
设计菜谱卡片
菜谱卡片是展示菜谱信息的基本单元,设计要简洁明了。
Widget _buildRecipeCard(int index) {
return Container(
width: 150.w,
margin: EdgeInsets.only(right: 12.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),
),
],
),
卡片宽度固定为 150.w,右侧留 12.w 的间距。背景色使用白色,在浅灰色的页面背景上能突出显示。
boxShadow 添加了一个轻微的阴影效果。阴影颜色使用半透明的灰色,模糊半径 5,向下偏移 2。这种阴影很微妙,但能让卡片有悬浮的感觉,增加层次感。
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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,
),
),
),
卡片顶部是图片区域,这里暂时用图标代替。实际应用中,这里应该显示菜谱的图片。容器高度 120.h,背景色使用浅橙色。
borderRadius 只设置顶部的圆角,这样和卡片的整体圆角配合,形成统一的视觉效果。图标居中显示,大小 50.sp,颜色使用深橙色。
Padding(
padding: EdgeInsets.all(8.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'美味菜谱 ${index + 1}',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Row(
children: [
Icon(Icons.timer, size: 12.sp, color: Colors.grey),
SizedBox(width: 4.w),
Text(
'30分钟',
style: TextStyle(fontSize: 11.sp, color: Colors.grey),
),
],
),
],
),
),
],
),
);
}
卡片底部显示菜谱的基本信息。标题使用粗体,字号 14.sp。maxLines 设置为 1,overflow 设置为 ellipsis,这样如果标题太长,会显示省略号。
下面一行显示烹饪时间,使用小图标配合文字。图标和文字都使用灰色,字号较小,作为辅助信息。这种设计让信息层次分明,用户一眼就能抓住重点。
优化滚动体验
首页内容较多,滚动体验很重要。SingleChildScrollView 默认的滚动效果已经不错,但我们还可以做一些优化。
比如可以添加滚动监听,当用户滚动到底部时自动加载更多内容:
final ScrollController _scrollController = ScrollController();
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// 加载更多
}
});
}
不过对于首页来说,内容是固定的,不需要分页加载。这个技巧更适合用在列表页面。
另一个优化是添加下拉刷新功能。用户下拉页面时,可以刷新首页内容:
RefreshIndicator(
onRefresh: () async {
// 刷新数据
},
child: SingleChildScrollView(
// ...
),
)
RefreshIndicator 会在用户下拉时显示一个加载指示器,并调用 onRefresh 回调。在回调中,我们可以重新获取数据,更新界面。
处理空状态
虽然首页通常都有内容,但我们还是要考虑空状态的情况。比如网络请求失败,或者数据还没加载完成。
可以定义一个状态变量来跟踪加载状态:
enum LoadingState { loading, success, error, empty }
LoadingState _state = LoadingState.loading;
然后根据状态显示不同的界面:
Widget build(BuildContext context) {
if (_state == LoadingState.loading) {
return Center(child: CircularProgressIndicator());
}
if (_state == LoadingState.error) {
return Center(child: Text('加载失败,请重试'));
}
if (_state == LoadingState.empty) {
return Center(child: Text('暂无内容'));
}
// 正常内容
}
这种处理方式让应用更健壮,即使出现异常情况,用户也能得到明确的反馈。
性能优化技巧
首页是用户最常访问的页面,性能优化尤为重要。
首先是图片优化。如果使用网络图片,要使用缓存机制,避免重复下载。可以使用 cached_network_image 包:
CachedNetworkImage(
imageUrl: recipe.imageUrl,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
其次是列表优化。ListView.builder 已经实现了懒加载,但如果列表项很复杂,还可以使用 AutomaticKeepAliveClientMixin 来保持列表项的状态。
最后是避免不必要的重建。使用 const 构造函数,把不变的组件标记为 const,这样 Flutter 可以复用它们。
响应式设计考虑
虽然我们使用了 flutter_screenutil 进行适配,但还要考虑一些特殊情况。
比如横屏模式,快速入口可能需要调整布局:
Widget _buildQuickEntry() {
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 600;
return isWide ? _buildWideLayout() : _buildNormalLayout();
},
);
}
LayoutBuilder 可以获取父容器的约束,根据宽度判断是否需要使用不同的布局。这样可以充分利用大屏幕的空间。
总结
首页的实现看似简单,但其中包含了很多设计考虑。从信息架构到视觉设计,从交互体验到性能优化,每个细节都影响着最终的效果。
我们使用了渐变色横幅吸引注意,快速入口提供便捷访问,内容区块展示丰富信息。整个页面结构清晰,操作流畅,能给用户留下良好的第一印象。
下一篇文章我们将实现推荐菜谱功能,深入探讨列表页面的设计和实现。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)