Flutter for OpenHarmony垃圾分类指南App实战:分类详情实现
垃圾分类详情页设计摘要 该页面通过动态颜色方案直观展示不同垃圾类型:可回收物为蓝色,有害垃圾为红色。页面结构分为顶部AppBar、分类信息区和物品列表三部分。AppBar背景色随分类类型变化,分类信息区包含图标、名称、描述和物品数量统计。物品列表采用卡片式设计,每个卡片显示物品图标和名称,点击可查看详情。页面通过路由参数接收垃圾类型,使用firstWhere方法匹配对应分类数据,实现类型化展示。整

从分类指南页面点击某个分类,就会进入这个分类详情页面。这里展示该分类的完整信息,包括分类说明和所有常见垃圾物品的列表。页面的颜色会根据分类类型动态变化,可回收物是蓝色调,有害垃圾是红色调,一眼就能认出来。
接收分类类型参数
页面通过路由参数接收垃圾类型:
class CategoryDetailPage extends StatelessWidget {
const CategoryDetailPage({super.key});
Widget build(BuildContext context) {
// 从路由参数获取分类类型
final GarbageType type = Get.arguments;
// 根据类型找到对应的分类数据
final category = GarbageData.categories.firstWhere((c) => c.type == type);
// 获取分类对应的颜色
final color = _getTypeColor(type);
Get.arguments拿到的是GarbageType枚举值,然后从GarbageData.categories里找到对应的分类数据。同时根据类型获取对应的主题颜色。
firstWhere的用法:
firstWhere会返回第一个满足条件的元素。这里用它来根据type找到对应的category对象。
动态颜色的AppBar
AppBar的背景色根据分类类型变化:
return Scaffold(
appBar: AppBar(
title: Text(category.name),
backgroundColor: color,
actions: [
// 搜索按钮,可以在当前分类内搜索
IconButton(
icon: const Icon(Icons.search),
onPressed: () => Get.toNamed(Routes.search),
),
],
),
进入可回收物详情,AppBar是蓝色;进入有害垃圾详情,AppBar是红色。这种设计让用户一眼就知道自己在看哪个分类。
页面主体分两部分:
body: Column(
children: [
// 头部信息区域
_buildHeader(category, color),
// 物品列表
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: category.items.length,
itemBuilder: (context, index) {
return _buildItemCard(category.items[index], color);
},
),
),
],
),
);
}
上面是头部信息区域,下面是物品列表。用Expanded包着ListView,让列表占据剩余空间。
头部信息区域
头部展示分类的图标、名称、描述和物品数量:
Widget _buildHeader(GarbageCategory category, Color color) {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
// 使用分类颜色的浅色版本作为背景
color: color.withOpacity(0.1),
),
child: Row(
children: [
// 分类图标
Container(
width: 72.w,
height: 72.w,
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(16.r),
),
child: Center(
child: Text(category.icon, style: TextStyle(fontSize: 40.sp)),
),
),
SizedBox(width: 16.w),
// 分类信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 分类名称
Text(
category.name,
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: color,
),
),
SizedBox(height: 4.h),
// 分类描述
Text(
category.description,
style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade600),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h),
// 物品数量标签
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
'${category.items.length} 个常见物品',
style: TextStyle(fontSize: 12.sp, color: color),
),
),
],
),
),
],
),
);
}
背景用分类颜色的10%透明度版本,跟AppBar形成呼应但不会太重。大号的emoji图标是视觉焦点,旁边是分类名称和描述。
颜色的层次:AppBar用纯色,头部背景用浅色,这样从上到下有个颜色的过渡,不会显得突兀。
物品列表卡片
每个垃圾物品显示为一个可点击的卡片:
Widget _buildItemCard(GarbageItem item, Color color) {
return Card(
margin: EdgeInsets.only(bottom: 8.h),
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
child: InkWell(
onTap: () => Get.toNamed(Routes.itemDetail, arguments: item),
borderRadius: BorderRadius.circular(12.r),
child: Padding(
padding: EdgeInsets.all(12.w),
child: Row(
children: [
// 物品图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Center(
child: Text(item.icon, style: TextStyle(fontSize: 28.sp)),
),
),
SizedBox(width: 12.w),
// 物品信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
),
SizedBox(height: 4.h),
Text(
item.description,
style: TextStyle(fontSize: 13.sp, color: Colors.grey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
// 箭头图标
Icon(Icons.arrow_forward_ios, size: 16.sp, color: Colors.grey),
],
),
),
),
);
}
用InkWell包裹实现点击水波纹效果。ListTile虽然方便,但自定义布局时用Row更灵活。
- leading:物品的emoji图标,背景用分类颜色的浅色版本
- title:物品名称
- subtitle:物品描述,限制一行,超出显示省略号
- trailing:箭头图标,提示可以点击
点击后跳转到物品详情页,把完整的GarbageItem对象传过去。
颜色映射方法
根据垃圾类型返回对应颜色:
Color _getTypeColor(GarbageType type) {
switch (type) {
case GarbageType.recyclable:
return AppTheme.recyclableColor;
case GarbageType.hazardous:
return AppTheme.hazardousColor;
case GarbageType.kitchen:
return AppTheme.kitchenColor;
case GarbageType.other:
return AppTheme.otherColor;
}
}
枚举匹配:这里直接用枚举值匹配,比用字符串匹配更安全。如果写错了枚举值,编译器会报错;写错字符串的话,编译器不会提醒。
数据模型的结构
分类数据的模型是这样定义的:
class GarbageCategory {
final String id;
final String name;
final GarbageType type;
final String description;
final String icon;
final List<GarbageItem> items;
GarbageCategory({
required this.id,
required this.name,
required this.type,
required this.description,
required this.icon,
this.items = const [],
});
}
每个分类包含:
- id:唯一标识
- name:分类名称,如"可回收物"
- type:分类类型枚举
- description:分类描述
- icon:emoji图标
- items:该分类下的所有垃圾物品列表
页面的信息层次
分类详情页面的设计体现了信息架构的层次性:
- 第一层:AppBar颜色 → 快速识别是哪个分类
- 第二层:头部信息 → 了解分类的基本情况
- 第三层:物品列表 → 查看具体有哪些垃圾
- 第四层:物品详情 → 深入了解某个垃圾的分类信息
用户可以根据自己的需求,在不同层次停留或深入。这种渐进式的信息展示,比一股脑把所有信息堆在一起要好得多。
列表性能优化
当分类下的物品很多时,需要考虑列表性能:
// 使用ListView.builder而不是ListView
ListView.builder(
itemCount: category.items.length,
itemBuilder: (context, index) {
return _buildItemCard(category.items[index], color);
},
);
// 如果需要更好的性能,可以使用ListView.separated
ListView.separated(
itemCount: category.items.length,
separatorBuilder: (context, index) => SizedBox(height: 8.h),
itemBuilder: (context, index) {
return _buildItemCard(category.items[index], color);
},
);
ListView.builder只会构建可见区域的item,滚动时动态创建和销毁,内存占用更低。
搜索功能的扩展
AppBar上的搜索按钮可以扩展为分类内搜索:
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// 方式1:跳转到搜索页,预设分类筛选
Get.toNamed(Routes.search, arguments: {'filterType': type});
// 方式2:在当前页面显示搜索框
showSearch(
context: context,
delegate: CategorySearchDelegate(category.items),
);
},
),
分类内搜索可以帮助用户在大量物品中快速找到目标。
空状态处理
如果某个分类下没有物品(虽然实际不太可能),需要显示空状态:
body: Column(
children: [
_buildHeader(category, color),
Expanded(
child: category.items.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64.sp, color: Colors.grey),
SizedBox(height: 16.h),
Text('暂无物品数据', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(...),
),
],
),
下拉刷新的实现
如果数据来自网络,可以添加下拉刷新:
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await guideController.refreshCategoryItems(type);
},
child: ListView.builder(...),
),
),
RefreshIndicator会在用户下拉时显示刷新指示器,刷新完成后自动隐藏。
与其他页面的导航关系
分类详情页在导航结构中的位置:
首页
└── 分类指南页
└── 分类详情页(当前)
└── 物品详情页
用户可以通过以下方式到达分类详情页:
- 从分类指南页点击某个分类
- 从首页的分类快捷入口
- 从搜索结果的分类标签
页面状态保持
如果用户从分类详情进入物品详情,再返回,列表的滚动位置应该保持:
// 方式1:使用PageStorageKey
ListView.builder(
key: PageStorageKey('category_${category.id}'),
// ...
);
// 方式2:使用ScrollController手动管理
final ScrollController _scrollController = ScrollController();
void dispose() {
_scrollController.dispose();
super.dispose();
}
PageStorageKey会自动保存和恢复滚动位置,是最简单的方案。
动画效果
可以为列表添加入场动画:
itemBuilder: (context, index) {
return AnimatedOpacity(
opacity: 1.0,
duration: Duration(milliseconds: 300 + index * 50),
child: _buildItemCard(category.items[index], color),
);
}
或者使用flutter_staggered_animations包实现更复杂的交错动画效果。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)