Flutter for OpenHarmony衣橱管家App实战:分类管理功能实现
衣橱管家App通过分类管理功能帮助用户清晰了解衣物构成。页面采用饼图展示分类占比,直观显示各类衣物数量,配合分类列表卡片显示具体数据。使用Flutter的fl_chart库实现环形饼图效果,通过颜色映射和图标系统增强可读性。分类卡片包含进度条显示占比,便于用户掌握购物偏好,避免冲动消费。该功能采用响应式设计,数据变化时自动更新图表和统计信息。

衣橱里的衣物越来越多,如果没有一个好的分类管理,找衣服就会变成一件头疼的事。今天我们来实现衣橱管家App的分类管理功能,帮助用户清晰地了解自己的衣物构成。
分类管理的价值
分类管理不仅仅是把衣物按类型分开,更重要的是让用户对自己的衣橱有一个全局的认知。通过统计数据和可视化图表,用户可以发现自己的购物偏好,比如是不是上衣买太多了,裤子却不够穿。
这种数据驱动的方式,能帮助用户做出更理性的购物决策,避免冲动消费。
页面整体结构
分类管理页面分为两部分:顶部是饼图展示各分类的占比,下面是分类列表显示具体数量。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../providers/wardrobe_provider.dart';
class CategoryScreen extends StatelessWidget {
const CategoryScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('分类管理')),
body: Consumer<WardrobeProvider>(
builder: (context, provider, child) {
final stats = provider.getCategoryStats();
final total = stats.values.fold(0, (a, b) => a + b);
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildPieChartCard(stats, total),
SizedBox(height: 16.h),
...stats.entries.map((entry) => _buildCategoryCard(entry.key, entry.value, total)),
],
),
);
},
),
);
}
}
导入了fl_chart包用于绑制饼图,这是Flutter中常用的图表库。Consumer监听Provider变化,getCategoryStats()返回各分类的数量统计。
fold方法计算所有分类数量的总和,用于后面计算百分比。SingleChildScrollView确保内容超出屏幕时可以滚动。
饼图卡片设计
饼图直观地展示各分类的占比,让用户一眼就能看出哪类衣物最多。
Widget _buildPieChartCard(Map<String, int> stats, int total) {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Text('衣物分类统计', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 16.h),
SizedBox(
height: 200.h,
child: stats.isEmpty
? const Center(child: Text('暂无数据'))
: PieChart(
PieChartData(
sections: _buildPieSections(stats, total),
centerSpaceRadius: 40.r,
sectionsSpace: 2,
),
),
),
],
),
),
);
}
饼图高度固定为200,centerSpaceRadius设为40形成环形图效果,比实心饼图更美观。sectionsSpace设为2,让各扇区之间有细微的间隔。
当没有数据时显示"暂无数据"提示,避免空白页面让用户困惑。
饼图扇区数据构建
每个分类对应饼图的一个扇区,颜色、大小和标签都需要动态计算。
List<PieChartSectionData> _buildPieSections(Map<String, int> stats, int total) {
final colors = [Colors.pink, Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.teal];
int index = 0;
return stats.entries.map((entry) {
final color = colors[index % colors.length];
index++;
return PieChartSectionData(
value: entry.value.toDouble(),
title: '${(entry.value / total * 100).toStringAsFixed(0)}%',
color: color,
radius: 50.r,
titleStyle: TextStyle(fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.bold),
);
}).toList();
}
预定义了6种颜色,使用取模运算循环使用,确保分类再多也有颜色可用。value是扇区的数值,决定扇区大小。
title显示百分比,toStringAsFixed(0)保留0位小数。titleStyle设置白色粗体,在彩色背景上清晰可见。
分类列表卡片
每个分类显示为一个卡片,包含图标、名称、进度条和数量。
Widget _buildCategoryCard(String category, int count, int total) {
final percent = total > 0 ? count / total : 0.0;
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFFE91E63).withOpacity(0.1),
child: Icon(_getCategoryIcon(category), color: const Color(0xFFE91E63)),
),
title: Text(category),
subtitle: LinearProgressIndicator(
value: percent,
backgroundColor: Colors.grey.shade200,
valueColor: const AlwaysStoppedAnimation(Color(0xFFE91E63)),
),
trailing: Text('$count件', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
),
);
}
ListTile是Material Design的列表项组件,leading放图标,title放分类名,subtitle放进度条,trailing放数量。
进度条直观显示该分类占总数的比例,backgroundColor是灰色底色,valueColor是品牌色填充。
分类图标映射
不同分类使用不同的图标,让用户更容易识别。
IconData _getCategoryIcon(String category) {
switch (category) {
case '上衣': return Icons.dry_cleaning;
case '裤子': return Icons.accessibility_new;
case '裙子': return Icons.woman;
case '外套': return Icons.checkroom;
case '鞋子': return Icons.ice_skating;
case '配饰': return Icons.watch;
default: return Icons.checkroom;
}
}
switch语句根据分类名称返回对应图标。上衣用干洗图标,裤子用人形图标,裙子用女性图标,以此类推。
default分支返回通用的衣架图标,处理未知分类的情况。这种映射方式比if-else更清晰。
添加图例说明
饼图旁边添加图例,说明每种颜色代表什么分类。
Widget _buildLegend(Map<String, int> stats) {
final colors = [Colors.pink, Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.teal];
int index = 0;
return Wrap(
spacing: 16.w,
runSpacing: 8.h,
children: stats.keys.map((category) {
final color = colors[index % colors.length];
index++;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12.w,
height: 12.w,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
SizedBox(width: 4.w),
Text(category, style: TextStyle(fontSize: 12.sp)),
],
);
}).toList(),
);
}
Wrap组件让图例自动换行,适应不同屏幕宽度。每个图例项包含一个彩色圆点和分类名称。
mainAxisSize设为min让Row只占用必要的宽度,多个图例可以并排显示。颜色顺序与饼图扇区保持一致。
点击分类查看详情
点击分类卡片可以查看该分类下的所有衣物。
Widget _buildCategoryCard(String category, int count, int total, WardrobeProvider provider) {
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: InkWell(
onTap: () {
final clothes = provider.clothes.where((c) => c.category == category).toList();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CategoryDetailScreen(category: category, clothes: clothes),
),
);
},
child: ListTile(
// ...
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('$count件', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
SizedBox(width: 8.w),
const Icon(Icons.chevron_right, color: Colors.grey),
],
),
),
),
);
}
InkWell包裹ListTile,点击时有水波纹效果。onTap中过滤出该分类的衣物,跳转到分类详情页。
trailing添加了右箭头图标,暗示用户这个卡片可以点击进入详情。
分类详情页
分类详情页展示该分类下的所有衣物,支持排序和筛选。
class CategoryDetailScreen extends StatelessWidget {
final String category;
final List<ClothingItem> clothes;
const CategoryDetailScreen({
super.key,
required this.category,
required this.clothes,
});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(category)),
body: clothes.isEmpty
? Center(child: Text('该分类暂无衣物'))
: GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 0.75,
),
itemCount: clothes.length,
itemBuilder: (context, index) {
return _buildClothingCard(context, clothes[index]);
},
),
);
}
}
分类详情页接收分类名称和衣物列表作为参数。AppBar标题显示分类名称,让用户知道当前在看哪个分类。
使用GridView展示衣物,与收藏页面保持一致的布局风格。
空分类处理
当某个分类没有衣物时,需要特殊处理。
Widget _buildEmptyCategoryHint(String category) {
return Card(
margin: EdgeInsets.only(bottom: 8.h),
color: Colors.grey.shade100,
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.grey.shade300,
child: Icon(_getCategoryIcon(category), color: Colors.grey),
),
title: Text(category, style: const TextStyle(color: Colors.grey)),
subtitle: const Text('暂无衣物'),
trailing: TextButton(
onPressed: () {
// 跳转到添加衣物页面,预选该分类
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AddClothingScreen(preselectedCategory: category),
),
);
},
child: const Text('添加'),
),
),
);
}
空分类使用灰色调显示,与有数据的分类形成区分。trailing放置添加按钮,引导用户添加该分类的衣物。
跳转到添加页面时传入预选分类,减少用户操作步骤。
分类统计趋势
可以展示分类数量的变化趋势,帮助用户了解购物习惯。
Widget _buildTrendChart(Map<String, List<int>> monthlyStats) {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('近6个月趋势', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 16.h),
SizedBox(
height: 150.h,
child: LineChart(
LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(show: false),
borderData: FlBorderData(show: false),
lineBarsData: _buildLineData(monthlyStats),
),
),
),
],
),
),
);
}
LineChart展示折线图,每条线代表一个分类的数量变化。gridData、titlesData、borderData都设为不显示,让图表更简洁。
这个功能需要记录每月的分类统计数据,实际项目中需要持久化存储历史数据。
分类管理操作
用户可能需要添加新分类或重命名现有分类。
void _showCategoryManageDialog(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add),
title: const Text('添加分类'),
onTap: () {
Navigator.pop(context);
_showAddCategoryDialog(context);
},
),
ListTile(
leading: const Icon(Icons.edit),
title: const Text('编辑分类'),
onTap: () {
Navigator.pop(context);
_showEditCategoryDialog(context);
},
),
ListTile(
leading: const Icon(Icons.delete),
title: const Text('删除分类'),
onTap: () {
Navigator.pop(context);
_showDeleteCategoryDialog(context);
},
),
],
),
);
}
底部弹窗提供三个操作选项:添加、编辑、删除。每个选项点击后关闭弹窗并打开对应的对话框。
删除分类需要谨慎处理,如果该分类下有衣物,需要提示用户先移动或删除衣物。
添加分类对话框
用户可以自定义新的分类。
void _showAddCategoryDialog(BuildContext context) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加分类'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: '请输入分类名称',
border: OutlineInputBorder(),
),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
if (controller.text.isNotEmpty) {
// 添加分类逻辑
Navigator.pop(context);
}
},
child: const Text('添加'),
),
],
),
);
}
autofocus设为true,对话框打开时输入框自动获得焦点,用户可以直接输入。
添加前检查输入是否为空,避免添加空分类。实际项目中还需要检查分类名是否重复。
分类排序功能
用户可以调整分类的显示顺序。
class _CategoryScreenState extends State<CategoryScreen> {
List<String> _categoryOrder = ['上衣', '裤子', '裙子', '外套', '鞋子', '配饰'];
Widget _buildReorderableList(Map<String, int> stats) {
return ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex--;
final item = _categoryOrder.removeAt(oldIndex);
_categoryOrder.insert(newIndex, item);
});
},
children: _categoryOrder.map((category) {
final count = stats[category] ?? 0;
return _buildCategoryCard(category, count, key: Key(category));
}).toList(),
);
}
}
ReorderableListView支持拖拽排序,长按卡片可以拖动调整位置。onReorder回调处理排序逻辑。
每个子项需要有唯一的Key,这里使用分类名称作为Key。排序结果应该持久化存储。
完整代码整合
把所有功能整合在一起,形成完整的分类管理页面。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../providers/wardrobe_provider.dart';
class CategoryScreen extends StatelessWidget {
const CategoryScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('分类管理')),
body: Consumer<WardrobeProvider>(
builder: (context, provider, child) {
final stats = provider.getCategoryStats();
final total = stats.values.fold(0, (a, b) => a + b);
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildPieChartCard(stats, total),
SizedBox(height: 16.h),
...stats.entries.map((entry) => _buildCategoryCard(entry.key, entry.value, total)),
],
),
);
},
),
);
}
Widget _buildPieChartCard(Map<String, int> stats, int total) {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Text('衣物分类统计', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 16.h),
SizedBox(
height: 200.h,
child: stats.isEmpty
? const Center(child: Text('暂无数据'))
: PieChart(
PieChartData(
sections: _buildPieSections(stats, total),
centerSpaceRadius: 40.r,
sectionsSpace: 2,
),
),
),
],
),
),
);
}
List<PieChartSectionData> _buildPieSections(Map<String, int> stats, int total) {
final colors = [Colors.pink, Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.teal];
int index = 0;
return stats.entries.map((entry) {
final color = colors[index % colors.length];
index++;
return PieChartSectionData(
value: entry.value.toDouble(),
title: '${(entry.value / total * 100).toStringAsFixed(0)}%',
color: color,
radius: 50.r,
titleStyle: TextStyle(fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.bold),
);
}).toList();
}
Widget _buildCategoryCard(String category, int count, int total) {
final percent = total > 0 ? count / total : 0.0;
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFFE91E63).withOpacity(0.1),
child: Icon(_getCategoryIcon(category), color: const Color(0xFFE91E63)),
),
title: Text(category),
subtitle: LinearProgressIndicator(
value: percent,
backgroundColor: Colors.grey.shade200,
valueColor: const AlwaysStoppedAnimation(Color(0xFFE91E63)),
),
trailing: Text('$count件', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
),
);
}
IconData _getCategoryIcon(String category) {
switch (category) {
case '上衣': return Icons.dry_cleaning;
case '裤子': return Icons.accessibility_new;
case '裙子': return Icons.woman;
case '外套': return Icons.checkroom;
case '鞋子': return Icons.ice_skating;
case '配饰': return Icons.watch;
default: return Icons.checkroom;
}
}
}
代码结构清晰,饼图和列表分别封装成独立方法。_buildPieSections处理饼图数据,_buildCategoryCard处理列表项。
使用fl_chart库绑制饼图,需要在pubspec.yaml中添加依赖。图表配置简洁,只保留必要的元素。
写在最后
分类管理功能帮助用户从宏观角度了解自己的衣橱构成。通过饼图和列表的结合,用户可以直观地看到各分类的占比和数量。
数据可视化是提升用户体验的重要手段,它让枯燥的数字变得生动有趣。希望这个功能能帮助用户更好地管理自己的衣橱。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)