Flutter for OpenHarmony轻量级开源记事本App实战:分类管理
分类是组织笔记的基础方式,通过合理的分类体系,用户可以将笔记按主题、项目或用途进行归类。一个好的分类管理系统应该简单直观,支持快速创建和切换。本文将详细介绍如何实现一个实用的分类管理功能。
分类页面的整体架构
分类页面集成了多种组织方式,包括分类、文件夹和标签的快捷入口。
class CategoryPage extends StatelessWidget {
const CategoryPage({super.key});
Widget build(BuildContext context) {
final controller = Get.find<NoteController>();
return Scaffold(
appBar: AppBar(
title: const Text('分类'),
actions: [
IconButton(
icon: const Icon(Icons.calendar_month),
onPressed: () => Get.to(() => const CalendarPage()),
),
],
),
页面使用StatelessWidget,状态管理交给GetX控制器。AppBar右侧添加日历图标,提供快速跳转到日历视图的入口。这种设计让用户可以在不同的组织视图之间快速切换。
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildQuickAccess(context, controller),
SizedBox(height: 24.h),
_buildCategorySection(context, controller),
SizedBox(height: 24.h),
_buildFolderSection(context, controller),
SizedBox(height: 24.h),
_buildTagSection(context, controller),
],
),
),
);
}
页面主体使用SingleChildScrollView支持滚动,内容分为四个模块:快捷入口、分类区域、文件夹区域和标签区域。每个模块之间用24像素的间距分隔,形成清晰的视觉层次。crossAxisAlignment设置为start让内容左对齐。这种模块化的布局让用户可以快速定位到需要的功能区域。
快捷入口的实现
快捷入口提供文件夹和标签的快速访问,显示数量统计。
Widget _buildQuickAccess(BuildContext context, NoteController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'快捷入口',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 12.h),
Row(
children: [
Expanded(
child: _QuickAccessCard(
icon: Icons.folder_outlined,
title: '文件夹',
count: controller.folders.length,
color: Colors.blue,
onTap: () => Get.to(() => const FolderPage()),
),
),
快捷入口使用Column布局,标题使用粗体大字号。下方是一个Row包含两个等宽的卡片。第一个卡片显示文件夹信息,使用蓝色主题。Expanded让卡片占据一半宽度,创建对称的布局。点击卡片跳转到文件夹列表页面,让用户可以查看和管理所有文件夹。
SizedBox(width: 12.w),
Expanded(
child: _QuickAccessCard(
icon: Icons.label_outlined,
title: '标签',
count: controller.tags.length,
color: Colors.green,
onTap: () => Get.to(() => const TagPage()),
),
),
],
),
],
);
}
第二个卡片显示标签信息,使用绿色主题与文件夹区分。两个卡片之间有12像素的间距。这种快捷入口的设计让用户可以一眼看到文件夹和标签的数量,并快速访问详情页面。颜色的区分也帮助用户快速识别不同的功能。
分类区域的构建
分类区域是页面的核心,展示所有用户创建的分类。
Widget _buildCategorySection(BuildContext context, NoteController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'分类',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showCreateCategoryDialog(context, controller),
),
],
),
分类区域的标题栏使用Row布局,左侧是标题,右侧是添加按钮。mainAxisAlignment设置为spaceBetween让两者分别靠左右两端。这种布局方式在Material Design中很常见,既美观又实用。点击添加按钮弹出创建分类对话框,让用户可以快速创建新分类。
SizedBox(height: 12.h),
Obx(() {
if (controller.categories.isEmpty) {
return Card(
child: Padding(
padding: EdgeInsets.all(24.w),
child: Center(
child: Text(
'暂无分类,点击右上角添加',
style: TextStyle(color: Colors.grey, fontSize: 14.sp),
),
),
),
);
}
使用Obx包裹内容实现响应式更新。当分类列表为空时,显示一个提示卡片,引导用户点击添加按钮创建分类。这种友好的空状态提示能够降低用户的学习成本,让新用户知道如何开始使用分类功能。
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 1.5,
),
itemCount: controller.categories.length,
分类列表使用GridView展示,两列布局。shrinkWrap设置为true让GridView根据内容自适应高度,physics设置为NeverScrollableScrollPhysics禁用GridView自身的滚动,使用外层的SingleChildScrollView统一管理。childAspectRatio设置为1.5,让卡片呈现横向矩形。这种网格布局能够在有限的空间内展示更多分类。
itemBuilder: (context, index) {
final category = controller.categories[index];
final noteCount = controller.getNotesByCategory(category.id).length;
return _CategoryCard(
name: category.name,
color: Color(int.parse(category.color.replaceFirst('#', '0xFF'))),
noteCount: noteCount,
onTap: () => Get.to(() => CategoryDetailPage(category: category)),
onLongPress: () => _showCategoryOptions(context, controller, category),
);
},
);
}),
],
);
}
itemBuilder为每个分类创建一个_CategoryCard组件。通过getNotesByCategory方法获取该分类下的笔记数量,将十六进制颜色字符串转换为Color对象。点击卡片跳转到分类详情页面,长按显示操作菜单。这种交互设计让用户可以快速查看分类内容或进行编辑删除操作。
文件夹区域的展示
文件夹区域显示根级文件夹的预览,不占用太多空间。
Widget _buildFolderSection(BuildContext context, NoteController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'文件夹',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
TextButton(
onPressed: () => Get.to(() => const FolderPage()),
child: const Text('查看全部'),
),
],
),
文件夹区域的标题栏与分类区域类似,但右侧使用TextButton显示"查看全部"文字。这种设计让用户明确知道点击后会看到完整的文件夹列表,比单纯的图标更加直观。文字按钮在这种场景下比图标按钮更容易理解。
SizedBox(height: 12.h),
Obx(() {
final rootFolders = controller.folders.where((f) => f.parentId == null).take(4).toList();
if (rootFolders.isEmpty) {
return Card(
child: Padding(
padding: EdgeInsets.all(24.w),
child: Center(
child: Text(
'暂无文件夹',
style: TextStyle(color: Colors.grey, fontSize: 14.sp),
),
),
),
);
}
通过where过滤出parentId为null的根级文件夹,使用take限制最多显示4个。这种预览式的展示既能让用户快速了解文件夹情况,又不会占用太多页面空间。如果没有文件夹,显示空状态提示。这种设计在保持页面简洁的同时提供了足够的信息。
return Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: rootFolders.map((f) => Chip(
avatar: const Icon(Icons.folder, size: 18),
label: Text(f.name),
)).toList(),
);
}),
],
);
}
文件夹使用Wrap组件展示,会自动换行。每个文件夹是一个Chip组件,包含文件夹图标和名称。spacing和runSpacing设置水平和垂直间距。Chip是Material Design中用于展示标签式内容的组件,非常适合这种预览场景。
标签区域的实现
标签区域的结构与文件夹类似,展示常用标签。
Widget _buildTagSection(BuildContext context, NoteController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'标签',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
TextButton(
onPressed: () => Get.to(() => const TagPage()),
child: const Text('查看全部'),
),
],
),
标签区域的标题栏与文件夹区域保持一致的设计风格,提供统一的用户体验。点击"查看全部"按钮会跳转到标签管理页面,用户可以在那里查看所有标签并进行管理操作。
SizedBox(height: 12.h),
Obx(() {
if (controller.tags.isEmpty) {
return Card(
child: Padding(
padding: EdgeInsets.all(24.w),
child: Center(
child: Text(
'暂无标签',
style: TextStyle(color: Colors.grey, fontSize: 14.sp),
),
),
),
);
}
return Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: controller.tags.take(10).map((t) => ActionChip(
label: Text('#${t.name}'),
onPressed: () {},
)).toList(),
);
}),
],
);
}
标签区域最多显示10个标签,使用ActionChip而不是普通Chip,表示这些标签是可点击的。标签名称前添加#符号,符合社交媒体的标签习惯。如果没有标签,显示空状态提示。这种设计让用户可以快速浏览常用标签。
创建分类对话框
创建分类对话框支持输入名称和选择颜色。
void _showCreateCategoryDialog(BuildContext context, NoteController controller) {
final nameController = TextEditingController();
String selectedColor = '#2196F3';
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('新建分类'),
使用StatefulBuilder包裹AlertDialog,让对话框内部可以使用setState更新UI。这对于颜色选择器这种需要实时反馈的交互非常重要。nameController管理分类名称输入,selectedColor存储用户选择的颜色,默认为蓝色。
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: '分类名称',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16.h),
对话框内容包含输入框和颜色选择器。TextField用于输入分类名称,使用OutlineInputBorder提供清晰的边框样式。mainAxisSize设置为min让Column只占用必要的空间,避免对话框过大。输入框和颜色选择器之间有16像素的间距。
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
'#F44336', '#E91E63', '#9C27B0', '#673AB7',
'#3F51B5', '#2196F3', '#03A9F4', '#00BCD4',
'#009688', '#4CAF50', '#8BC34A', '#CDDC39',
'#FFEB3B', '#FFC107', '#FF9800', '#FF5722',
].map((c) => GestureDetector(
onTap: () => setState(() => selectedColor = c),
颜色选择器提供16种预设颜色,涵盖了Material Design的主要色系。使用Wrap布局让颜色块自动换行。GestureDetector处理点击事件,点击后更新selectedColor并刷新UI。这种即时反馈让用户清楚地知道当前选择的是哪个颜色。
child: Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: Color(int.parse(c.replaceFirst('#', '0xFF'))),
shape: BoxShape.circle,
border: selectedColor == c
? Border.all(color: Colors.black, width: 2)
: null,
),
),
)).toList(),
),
],
),
每个颜色块是一个32x32的圆形容器。当颜色被选中时,添加黑色边框作为视觉反馈。shape设置为circle创建圆形。这种设计让颜色选择器既美观又易用,用户可以清楚地看到每个颜色的效果。
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
if (nameController.text.isNotEmpty) {
controller.createCategory(nameController.text, selectedColor);
Navigator.pop(context);
}
},
child: const Text('创建'),
),
],
),
),
);
}
对话框底部有取消和创建两个按钮。取消按钮使用TextButton,创建按钮使用ElevatedButton,形成主次分明的视觉层次。创建按钮会验证名称不为空,然后调用控制器的createCategory方法创建分类,最后关闭对话框。这种简单的验证能够避免创建空名称的分类。
分类操作菜单
长按分类卡片会显示操作菜单,提供编辑和删除功能。
void _showCategoryOptions(BuildContext context, NoteController controller, category) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.edit),
title: const Text('编辑'),
onTap: () {
Navigator.pop(context);
// TODO: Edit category
},
),
使用ModalBottomSheet显示操作菜单,这是Material Design推荐的移动端操作菜单样式。SafeArea确保内容不会被系统UI遮挡。编辑选项目前是TODO状态,后续可以实现编辑分类名称和颜色的功能。mainAxisSize设置为min让菜单只占用必要的高度。
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: const Text('删除', style: TextStyle(color: Colors.red)),
onTap: () {
Navigator.pop(context);
controller.deleteCategory(category.id);
},
),
],
),
),
);
}
}
删除选项使用红色突出显示,警示用户这是一个危险操作。点击后先关闭底部菜单,然后调用控制器的deleteCategory方法删除分类。这种设计让用户在执行删除操作时更加谨慎,避免误操作。
快捷访问卡片组件
_QuickAccessCard是一个自定义组件,用于展示快捷入口。
class _QuickAccessCard extends StatelessWidget {
final IconData icon;
final String title;
final int count;
final Color color;
final VoidCallback onTap;
const _QuickAccessCard({
required this.icon,
required this.title,
required this.count,
required this.color,
required this.onTap,
});
组件接收图标、标题、数量、颜色和点击回调作为参数。这种参数化的设计让组件可以灵活复用,文件夹和标签卡片使用相同的组件但显示不同的内容。使用StatelessWidget是因为卡片本身不维护状态。
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 28.sp),
SizedBox(height: 8.h),
Card提供卡片样式,InkWell添加点击效果和水波纹动画。borderRadius设置圆角与Card的圆角保持一致。图标使用传入的颜色和较大的尺寸,作为卡片的视觉焦点。crossAxisAlignment设置为start让内容左对齐。
Text(
title,
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
Text(
'$count 项',
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
],
),
),
),
);
}
}
标题使用较大的字号和粗体,数量信息使用小字号和灰色,形成清晰的信息层次。这种设计让用户可以快速扫描并获取关键信息。卡片内部使用Column布局,垂直排列图标、标题和数量。
分类卡片组件
_CategoryCard用于展示单个分类,包含名称、颜色和笔记数量。
class _CategoryCard extends StatelessWidget {
final String name;
final Color color;
final int noteCount;
final VoidCallback onTap;
final VoidCallback onLongPress;
const _CategoryCard({
required this.name,
required this.color,
required this.noteCount,
required this.onTap,
required this.onLongPress,
});
组件接收分类名称、颜色、笔记数量以及点击和长按回调。onLongPress用于显示操作菜单,提供编辑和删除功能。这种设计让卡片既能快速访问分类内容,又能进行管理操作。
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: EdgeInsets.all(12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
backgroundColor: color,
radius: 16.w,
child: Icon(Icons.folder, color: Colors.white, size: 18.sp),
),
InkWell同时处理点击和长按事件。CircleAvatar使用分类的颜色作为背景,内部显示白色的文件夹图标。这种设计让每个分类都有独特的视觉标识,用户可以通过颜色快速识别不同的分类。
SizedBox(height: 8.h),
Text(
name,
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'$noteCount 篇笔记',
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
],
),
),
),
);
}
}
分类名称限制为单行显示,超出部分用省略号表示。笔记数量使用灰色小字显示,让用户了解每个分类的使用情况。mainAxisAlignment设置为center让内容垂直居中。这种信息密度适中的设计既不会显得空洞,也不会让用户感到信息过载。
分类管理是笔记应用的核心功能之一,它提供了一种结构化的组织方式。通过合理的分类体系,用户可以将笔记按主题、项目或用途进行归类,快速找到需要的内容。本文介绍的分类管理系统不仅支持基础的增删改查,还集成了文件夹和标签的快捷入口,为用户提供了完整的笔记组织解决方案。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)