Flutter for OpenHarmony生活助手App实战:分类管理功能实现
本文介绍了生活助手App中分类管理功能的设计与实现。作者基于个人记账痛点,提出让用户自定义分类的重要性,包括满足个性化需求、提高数据准确性和增强视觉识别。功能设计采用支出/收入分类分开管理、可视化图标颜色选择、分类编辑删除等方案。技术实现上使用Flutter的DefaultTabController、TabBar和ListView.builder等组件,构建了包含分类卡片、图标容器和编辑按钮的交互

我自己记账的时候,最头疼的就是分类。有些App预设的分类太少,有些又太多太乱。后来我发现,最好的方案是让用户自己管理分类,想加什么加什么,不喜欢的可以删掉。所以在做这个生活助手App时,我特别重视分类管理这个功能。
为什么分类管理这么重要
在开始写代码之前,我先想清楚了这个功能的价值。
第一个是个性化需求。每个人的消费习惯不一样,需要的分类也不同。比如我经常点外卖,就需要一个"外卖"分类;有人经常买书,就需要一个"图书"分类。预设的分类很难满足所有人的需求。
第二个是数据准确性。分类越细致,统计出来的数据就越准确。如果只有一个"其他"分类,时间长了就不知道钱花哪儿了。但如果分类太多,记账时又要花很多时间选择。所以要让用户自己掌控分类的粒度。
第三个是视觉识别。每个分类配上不同的图标和颜色,记账时一眼就能找到。这比纯文字列表效率高多了。我自己用的时候,看到橙色的餐饮图标,手指就自动点过去了,根本不用思考。
功能设计的思路
在设计这个功能时,我考虑了以下几个方面。
支出和收入分开管理
支出和收入的分类是不同的。支出有餐饮、交通、购物等,收入有工资、奖金、投资等。如果混在一起,会很乱。所以我用了TabBar,把两种分类分成两个标签页。
图标和颜色的可视化
每个分类都有自己的图标和颜色。图标用Flutter内置的Material Icons,颜色从预设的几种里选。这样既统一又有辨识度。
编辑和删除功能
用户可以编辑分类的名称、图标、颜色,也可以删除不需要的分类。但系统预设的分类不能删除,避免用户误操作后找不回来。
添加新分类
右下角的悬浮按钮,点击可以添加新分类。弹出一个对话框,让用户输入名称、选择图标和颜色。
页面整体结构
先看页面的基本框架:
class CategoryManagementPage extends StatelessWidget {
const CategoryManagementPage({super.key});
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('分类管理'),
bottom: const TabBar(
tabs: [
Tab(text: '支出分类'),
Tab(text: '收入分类'),
],
),
),
body: TabBarView(
children: [
_buildCategoryList(true),
_buildCategoryList(false),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
),
);
}
}
DefaultTabController的使用
DefaultTabController是Flutter提供的标签页控制器,length: 2表示有两个标签。它会自动管理标签的切换,不需要我们手动处理状态。
TabBar和TabBarView的配合
TabBar放在AppBar的bottom位置,显示两个标签:“支出分类"和"收入分类”。TabBarView是标签对应的内容,两个标签对应两个页面。
当用户点击标签时,TabBarView会自动切换到对应的页面。这个切换是有动画的,从左往右或从右往左滑动,体验很流畅。
FloatingActionButton的位置
悬浮按钮固定在右下角,无论切换到哪个标签,都能看到。这样用户随时可以添加新分类,不用在菜单里找。
分类列表的构建
两个标签页的内容是类似的,只是数据不同。我用一个方法来构建,传入一个布尔值区分是支出还是收入:
Widget _buildCategoryList(bool isExpense) {
final categories = isExpense
? [
{'name': '餐饮', 'icon': Icons.restaurant, 'color': Colors.orange},
{'name': '交通', 'icon': Icons.directions_car, 'color': Colors.blue},
{'name': '购物', 'icon': Icons.shopping_bag, 'color': Colors.purple},
{'name': '娱乐', 'icon': Icons.movie, 'color': Colors.pink},
{'name': '住房', 'icon': Icons.home, 'color': Colors.green},
{'name': '医疗', 'icon': Icons.local_hospital, 'color': Colors.red},
]
: [
{'name': '工资', 'icon': Icons.work, 'color': Colors.green},
{'name': '奖金', 'icon': Icons.card_giftcard, 'color': Colors.amber},
{'name': '投资', 'icon': Icons.trending_up, 'color': Colors.blue},
{'name': '其他', 'icon': Icons.more_horiz, 'color': Colors.grey},
];
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
return _buildCategoryCard(category);
},
);
}
三元表达式的妙用
isExpense ? [...] : [...]这个三元表达式根据参数返回不同的数据。如果是支出分类,返回餐饮、交通等;如果是收入分类,返回工资、奖金等。
这样一个方法就能处理两种情况,代码复用率高。
支出分类的选择
支出分类我选了6个常用的:餐饮、交通、购物、娱乐、住房、医疗。这是根据大多数人的消费习惯总结的。
- 餐饮:包括外卖、聚餐、买菜等,用橙色,因为橙色让人联想到食物
- 交通:包括打车、公交、地铁、加油等,用蓝色,蓝色给人移动的感觉
- 购物:包括衣服、日用品、电子产品等,用紫色,紫色比较时尚
- 娱乐:包括电影、游戏、旅游等,用粉色,粉色比较轻松愉快
- 住房:包括房租、物业费、装修等,用绿色,绿色代表家的温馨
- 医疗:包括看病、买药、体检等,用红色,红色是医疗的标志色
收入分类的选择
收入分类相对简单,4个就够了:工资、奖金、投资、其他。
- 工资:固定收入,用绿色,绿色代表稳定
- 奖金:额外收入,用琥珀色,琥珀色给人惊喜的感觉
- 投资:理财收益,用蓝色,蓝色代表理性
- 其他:其他来源,用灰色,灰色比较中性
ListView.builder的使用
ListView.builder是懒加载的列表,只会构建可见的item。虽然这里分类数量不多,但养成好习惯总是对的。
分类卡片的设计
每个分类用一个卡片来展示,这个卡片的设计我反复调整了好几次:
Widget _buildCategoryCard(Map<String, dynamic> category) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: (category['color'] as Color).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
category['icon'] as IconData,
color: category['color'] as Color,
size: 28.sp,
),
),
SizedBox(width: 16.w),
Expanded(
child: Text(
category['name'] as String,
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
),
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {},
),
],
),
);
}
图标容器的设计
图标外面包了一个Container,背景色是分类颜色的10%透明度。这样既有颜色区分,又不会太刺眼。比如餐饮的橙色,背景就是淡淡的橙色。
borderRadius: BorderRadius.circular(12.r)让容器变成圆角矩形,和整体的圆润风格保持一致。
图标的大小和颜色
图标大小设置为28.sp,不大不小刚刚好。颜色用分类的主题色,和背景色呼应。
分类名称的布局
分类名称用Expanded包裹,这样它会占据剩余的所有空间。如果名称很长,会自动换行,不会被截断。
fontWeight: FontWeight.bold让文字加粗,更醒目。
编辑按钮的位置
右边放了一个编辑按钮,点击可以修改分类。用IconButton而不是普通的Icon,因为需要点击事件。
添加新分类的交互
右下角的悬浮按钮,点击后应该弹出一个对话框,让用户输入分类信息。虽然代码里onPressed是空的,但实际项目中应该这样实现:
void _showAddCategoryDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加分类'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
labelText: '分类名称',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16.h),
const Text('选择图标'),
// 这里应该放一个图标选择器
SizedBox(height: 16.h),
const Text('选择颜色'),
// 这里应该放一个颜色选择器
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
// 保存新分类
Navigator.pop(context);
},
child: const Text('添加'),
),
],
),
);
}
对话框的内容
对话框包含三个部分:输入框、图标选择器、颜色选择器。
输入框用TextField,设置了labelText和border,看起来更正式。
图标选择器可以用一个网格布局,展示所有可用的图标。用户点击选中一个。
颜色选择器可以用一排圆形色块,用户点击选中一个颜色。
保存逻辑
点击"添加"按钮后,应该:
- 获取输入框的内容
- 验证输入是否为空
- 创建新的分类对象
- 添加到分类列表
- 保存到数据库
- 关闭对话框
- 刷新列表
编辑分类的实现
每个分类卡片右边的编辑按钮,点击后应该弹出类似的对话框,但是预填充当前分类的信息:
void _showEditCategoryDialog(BuildContext context, Map<String, dynamic> category) {
final nameController = TextEditingController(text: category['name'] as String);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('编辑分类'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: '分类名称',
border: OutlineInputBorder(),
),
),
// 图标和颜色选择器,预选中当前的图标和颜色
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
// 更新分类信息
Navigator.pop(context);
},
child: const Text('保存'),
),
],
),
);
}
TextEditingController的使用
TextEditingController可以控制输入框的内容。创建时传入text参数,输入框就会预填充这个文字。
用户修改后,通过nameController.text可以获取新的内容。
预选中图标和颜色
图标选择器和颜色选择器应该预选中当前分类的图标和颜色。这样用户知道当前的设置是什么,可以选择保持不变或者修改。
删除分类的功能
长按分类卡片,应该弹出一个确认对话框,询问是否删除:
void _showDeleteConfirmDialog(BuildContext context, Map<String, dynamic> category) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除分类'),
content: Text('确定要删除"${category['name']}"分类吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
// 删除分类
Navigator.pop(context);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('删除'),
),
],
),
);
}
确认对话框的必要性
删除是不可逆的操作,必须让用户确认。对话框的内容要明确告诉用户要删除哪个分类。
删除按钮用红色,警示用户这是危险操作。
删除的限制
系统预设的分类不应该允许删除。可以在分类数据里加个isSystem字段,标记是否是系统分类。如果是,长按时不弹出删除对话框,或者弹出提示"系统分类不能删除"。
数据持久化的考虑
分类数据应该保存到本地数据库,这样应用关闭后再打开,分类还在。
使用SQLite存储
可以用sqflite包来操作SQLite数据库。创建一个categories表,包含以下字段:
id:主键,自增name:分类名称icon:图标名称(存字符串,使用时转换成IconData)color:颜色值(存整数,使用时转换成Color)type:类型(0表示支出,1表示收入)isSystem:是否系统分类(0表示用户自定义,1表示系统预设)sortOrder:排序顺序
初始化预设分类
应用第一次启动时,应该插入预设的分类。可以在数据库初始化时检查categories表是否为空,如果为空就插入预设数据。
分类的排序
用户可能想调整分类的顺序,把常用的放在前面。可以加个拖拽排序的功能,用ReorderableListView实现。
图标选择器的实现
添加和编辑分类时,需要一个图标选择器。可以用GridView展示所有可用的图标:
Widget _buildIconPicker(IconData? selectedIcon, Function(IconData) onSelect) {
final icons = [
Icons.restaurant,
Icons.directions_car,
Icons.shopping_bag,
Icons.movie,
Icons.home,
Icons.local_hospital,
Icons.work,
Icons.card_giftcard,
Icons.trending_up,
Icons.more_horiz,
// 更多图标...
];
return GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: icons.length,
itemBuilder: (context, index) {
final icon = icons[index];
final isSelected = icon == selectedIcon;
return GestureDetector(
onTap: () => onSelect(icon),
child: Container(
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: isSelected ? Colors.white : Colors.black,
),
),
);
},
);
}
GridView的布局
crossAxisCount: 5表示每行显示5个图标。mainAxisSpacing和crossAxisSpacing设置间距。
选中状态的视觉反馈
选中的图标背景色是蓝色,未选中的是灰色。这样用户一眼就能看出选了哪个。
颜色选择器的实现
颜色选择器比图标选择器简单,就是一排圆形色块:
Widget _buildColorPicker(Color? selectedColor, Function(Color) onSelect) {
final colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.pink,
Colors.brown,
Colors.grey,
];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: colors.map((color) {
final isSelected = color == selectedColor;
return GestureDetector(
onTap: () => onSelect(color),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected ? Border.all(color: Colors.black, width: 3) : null,
),
),
);
}).toList(),
);
}
圆形色块的实现
shape: BoxShape.circle让容器变成圆形。选中的色块加个黑色边框,未选中的没有边框。
颜色的选择
我选了9种常用的颜色,基本能满足需求。如果想要更多颜色,可以加个"自定义颜色"选项,弹出一个完整的颜色选择器。
实际使用体验
这个功能我自己用了一段时间,感觉还不错。预设的分类基本够用,偶尔需要加个新分类也很方便。
图标和颜色的可视化很重要,记账时不用看文字,看图标就知道是哪个分类。这大大提高了记账的效率。
有一次我想加个"宠物"分类,因为养了只猫,每个月猫粮、猫砂、看病的花费不少。点击悬浮按钮,输入名称,选了个猫爪图标,选了粉色,几秒钟就搞定了。
可以改进的地方
如果要做得更完善,可以考虑以下几点。
分类的统计信息
在分类卡片上显示这个分类的总支出或总收入,以及占比。这样用户能知道哪个分类花钱最多。
分类的使用频率排序
把最常用的分类排在前面,不常用的排在后面。可以根据使用次数自动排序,或者让用户手动拖拽排序。
分类的图标库扩展
Material Icons虽然很全,但有些场景还是不够用。可以支持自定义图标,让用户上传图片作为图标。
分类的导入导出
用户可能在多个设备上使用,或者想分享自己的分类设置给朋友。可以加个导入导出功能,把分类数据导出成JSON文件。
分类的预设模板
除了默认的分类,可以提供几套预设模板,比如"学生模板"、“上班族模板”、“家庭主妇模板”。用户可以一键导入,省去手动添加的麻烦。
小结
今天实现了分类管理功能,用到了标签页、列表、对话框等组件。核心是用TabBar和TabBarView分别管理支出和收入分类,用图标和颜色增强视觉识别。
这个功能虽然不是最核心的,但对用户体验影响很大。分类设置得好,记账就快;分类设置得乱,记账就慢。让用户自己管理分类,是最灵活的方案。
在实现过程中,我特别注重可视化。每个分类都有自己的图标和颜色,记账时一眼就能找到。这种细节上的优化,能大大提高用户的使用效率。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)