Flutter for OpenHarmony轻量级开源记事本App实战:模板列表
模板列表是笔记应用的重要功能,它为用户提供了快速创建笔记的预设格式。通过使用模板,用户可以避免重复输入相同的内容结构,提高笔记创建的效率。本文将详细介绍如何实现一个功能完善的模板列表系统。
模板列表的设计理念
模板列表的核心功能是展示和管理各种笔记模板。设计上需要清晰地区分不同类型的模板,让用户能够快速找到需要的模板。模板应该包含预览信息,让用户了解使用该模板后的笔记结构。
在视觉设计上,模板列表应该简洁明了,突出显示模板名称和预览内容。使用卡片式布局展示模板,每个卡片包含图标、标题、预览内容和操作按钮。这种设计让用户可以快速浏览和选择模板。
模板页面的基础框架
首先构建模板列表页面的基本结构:
class TemplatesPage extends StatelessWidget {
const TemplatesPage({super.key});
Widget build(BuildContext context) {
final controller = Get.find<NoteController>();
return Scaffold(
appBar: AppBar(
title: const Text('笔记模板'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showCreateTemplateDialog(context, controller),
),
],
),
TemplatesPage采用StatelessWidget设计,通过GetX的依赖注入获取NoteController实例。这种设计模式将状态管理与UI分离,提高了代码的可维护性。AppBar中的添加按钮允许用户快速创建新模板,点击后会弹出创建对话框。Scaffold提供了标准的Material Design布局结构,确保界面符合设计规范。
继续构建列表展示部分:
body: Obx(() => ListView.builder(
padding: EdgeInsets.all(12.w),
itemCount: controller.templates.length,
itemBuilder: (context, index) {
final template = controller.templates[index];
return _buildTemplateCard(template, controller);
},
)),
);
}
}
body部分使用Obx包裹ListView.builder实现响应式更新。当controller.templates发生变化时,列表会自动重新渲染。ListView.builder采用懒加载机制,只构建可见区域的列表项,提高了大量模板时的性能表现。每个模板通过_buildTemplateCard方法渲染为独立的卡片组件。
模板卡片的头部设计
模板卡片的头部包含图标和标题信息:
Widget _buildTemplateCard(NoteTemplate template, NoteController controller) {
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
leading: CircleAvatar(
backgroundColor: _getTemplateColor(template.type).withOpacity(0.1),
child: Icon(
_getTemplateIcon(template.type),
color: _getTemplateColor(template.type),
),
),
_buildTemplateCard方法创建模板卡片的核心结构。Card组件提供了阴影和圆角效果,增强视觉层次感。leading部分使用CircleAvatar展示模板类型图标,背景色采用半透明设计,与图标颜色保持一致性。这种设计让用户能够通过颜色快速识别不同类型的模板,提升了用户体验。
模板卡片的内容区域
构建模板的标题和预览内容:
title: Text(
template.name,
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 4.h),
Text(
template.content.isEmpty ? '空白模板' : template.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
title显示模板名称,使用中等字重突出显示。subtitle采用Column布局,包含多行信息。模板内容预览限制为2行,超出部分使用省略号显示,避免占用过多空间。空白模板会显示提示文字,让用户清楚了解模板的状态。这种设计在保持信息完整性的同时,确保了界面的简洁性。
继续添加使用统计信息:
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
SizedBox(height: 4.h),
Text(
'使用 ${template.usageCount} 次',
style: TextStyle(fontSize: 10.sp, color: Colors.grey.shade500),
),
],
),
使用次数统计帮助用户了解模板的受欢迎程度。较小的字号和灰色文字表明这是次要信息,不会干扰主要内容的阅读。这个统计数据可以作为用户选择模板的参考依据,常用的模板往往更加实用。通过展示使用频率,系统还能引导用户发现高质量的模板。
模板卡片的操作按钮
添加编辑和删除操作:
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditTemplateDialog(context, controller, template),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _confirmDeleteTemplate(context, controller, template),
),
],
),
trailing区域放置操作按钮,使用Row布局水平排列。mainAxisSize.min确保Row只占用必要的空间。编辑按钮使用默认图标颜色,删除按钮使用红色强调其危险性。这种视觉差异化设计能够有效防止用户误操作。IconButton提供了合适的点击区域,符合移动端的交互规范。
添加点击事件处理:
onTap: () {
final note = controller.createNote(templateId: template.id);
Get.to(() => NoteEditorPage(note: note));
},
),
);
}
onTap事件处理卡片的点击操作。点击卡片时,系统会基于选中的模板创建新笔记,并立即跳转到编辑器页面。这种一步到位的交互设计简化了用户操作流程,提高了使用效率。createNote方法接收templateId参数,确保新笔记继承模板的内容和格式。Get.to实现页面导航,保持了GetX框架的一致性。
模板类型枚举定义
定义模板类型及其属性:
enum TemplateType {
meeting('会议记录', Icons.meeting_room, Color(0xFF2196F3)),
diary('日记', Icons.book, Color(0xFF4CAF50)),
todo('待办事项', Icons.checklist, Color(0xFFFF9800)),
study('学习笔记', Icons.school, Color(0xFF9C27B0)),
work('工作报告', Icons.work, Color(0xFFF44336)),
travel('旅行计划', Icons.flight, Color(0x00BCD4)),
custom('自定义', Icons.description, Color(0xFF607D8B));
TemplateType枚举定义了七种常见的模板类型,每种类型都关联了显示名称、图标和主题色。这种设计将类型的视觉表现与数据模型紧密结合,避免了在多处维护相同信息。颜色选择遵循Material Design色彩规范,确保视觉和谐。枚举的使用提供了类型安全,防止了无效的模板类型值。
继续定义枚举的属性:
const TemplateType(this.displayName, this.icon, this.color);
final String displayName;
final IconData icon;
final Color color;
}
枚举的构造函数和属性定义使每个类型值都携带完整的展示信息。displayName用于界面显示,icon定义视觉标识,color确定主题色调。这种封装方式让代码更加简洁,使用时只需引用枚举值即可获取所有相关属性。const构造函数确保了枚举值的不可变性和编译时常量特性。
模板类型选择器组件
创建类型选择器的基础结构:
class TemplateTypeSelector extends StatelessWidget {
final TemplateType selectedType;
final Function(TemplateType) onTypeChanged;
const TemplateTypeSelector({
super.key,
required this.selectedType,
required this.onTypeChanged,
});
Widget build(BuildContext context) {
TemplateTypeSelector是一个可复用的类型选择组件。通过selectedType属性接收当前选中的类型,通过onTypeChanged回调通知父组件类型变化。这种设计遵循了Flutter的单向数据流原则,组件本身不维护状态,而是由父组件控制。required关键字确保必要参数不会被遗漏,提高了代码的健壮性。
实现类型选项的布局:
return Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: TemplateType.values.map((type) =>
_buildTypeOption(type)
).toList(),
);
}
Wrap组件实现了自适应的流式布局,当一行空间不足时自动换行。spacing和runSpacing分别控制水平和垂直间距,确保选项之间有适当的留白。通过遍历TemplateType.values获取所有类型值,为每个类型创建选项按钮。这种动态生成方式确保了新增类型时无需修改UI代码。
类型选项的视觉设计
构建单个类型选项的外观:
Widget _buildTypeOption(TemplateType type) {
final isSelected = selectedType == type;
return GestureDetector(
onTap: () => onTypeChanged(type),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: isSelected
? type.color.withOpacity(0.1)
: Colors.grey.withOpacity(0.1),
_buildTypeOption方法为每个类型创建可点击的选项按钮。isSelected变量判断当前类型是否被选中,用于控制视觉状态。GestureDetector处理点击事件,Container提供视觉容器。选中状态使用类型的主题色作为背景,未选中状态使用灰色,通过颜色差异清晰地表达选择状态。半透明背景避免了颜色过于浓重。
继续设置边框和圆角:
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? type.color
: Colors.grey.withOpacity(0.3),
),
),
圆角设计使选项按钮更加柔和友好,16的圆角半径提供了适中的视觉效果。边框颜色同样根据选中状态变化,选中时使用类型的完整主题色,未选中时使用淡灰色。这种双重视觉反馈(背景色+边框色)让选择状态更加明确,即使在不同光线条件下也能清晰辨识。
添加图标和文字内容:
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
type.icon,
color: isSelected ? type.color : Colors.grey.shade600,
size: 16.sp,
),
SizedBox(width: 6.w),
Text(
type.displayName,
Row布局将图标和文字水平排列,mainAxisSize.min确保按钮大小适应内容。图标颜色根据选中状态变化,选中时使用主题色,未选中时使用中灰色。图标大小设置为16.sp,与文字大小协调。SizedBox提供图标和文字之间的间距,6.w的宽度让布局既紧凑又不拥挤。
完成文字样式设置:
style: TextStyle(
fontSize: 12.sp,
color: isSelected ? type.color : Colors.black,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}
}
文字样式同样响应选中状态的变化。选中时使用主题色和较粗的字重,未选中时使用黑色和正常字重。这种多维度的视觉反馈(颜色、字重、背景、边框)确保了选择状态的高度可识别性。12.sp的字号适合移动端阅读,既不会太小难以辨认,也不会太大占用过多空间。
创建模板对话框的初始化
设置对话框的基础结构:
void _showCreateTemplateDialog(BuildContext context, NoteController controller) {
final nameController = TextEditingController();
final contentController = TextEditingController();
TemplateType selectedType = TemplateType.custom;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('创建模板'),
_showCreateTemplateDialog方法显示创建模板的对话框。创建两个TextEditingController分别管理名称和内容输入。selectedType初始化为custom类型,作为默认选项。StatefulBuilder允许对话框内部维护状态,实现类型选择的实时更新。AlertDialog提供标准的Material Design对话框样式,title明确告知用户当前操作。
构建对话框的内容区域:
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: '模板名称',
border: OutlineInputBorder(),
content使用SingleChildScrollView包裹,确保内容过多时可以滚动。Column的mainAxisSize.min让对话框高度适应内容。crossAxisAlignment.start使所有元素左对齐。TextField用于输入模板名称,OutlineInputBorder提供带边框的输入框样式,labelText作为浮动标签提示用户。这种设计符合Material Design规范,提供了清晰的输入引导。
添加类型选择区域:
),
),
SizedBox(height: 16.h),
Text(
'模板类型',
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500),
),
SizedBox(height: 8.h),
TemplateTypeSelector(
selectedType: selectedType,
onTypeChanged: (type) => setState(() => selectedType = type),
),
SizedBox提供输入框之间的垂直间距,16.h的高度让布局更加舒适。Text组件显示"模板类型"标签,使用中等字重突出显示。TemplateTypeSelector组件嵌入对话框中,通过setState回调更新选中的类型。这种组件化设计提高了代码复用性,同时保持了对话框内部状态的响应性。
添加内容输入区域:
SizedBox(height: 16.h),
TextField(
controller: contentController,
maxLines: 5,
decoration: const InputDecoration(
labelText: '模板内容',
border: OutlineInputBorder(),
hintText: '输入模板的预置内容...',
),
),
],
),
),
内容输入框设置maxLines为5,提供多行输入空间。hintText提供输入提示,引导用户理解这里应该填写什么。OutlineInputBorder保持了与名称输入框一致的视觉风格。多行输入适合模板内容的特点,用户可以预先设置笔记的结构和常用文本。这种设计让模板功能更加实用。
对话框的操作按钮
实现取消和创建按钮:
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
if (nameController.text.isNotEmpty) {
controller.createTemplate(
name: nameController.text,
content: contentController.text,
type: selectedType,
);
actions区域包含操作按钮。TextButton用于取消操作,点击后关闭对话框。ElevatedButton用于确认创建,使用凸起样式强调主要操作。创建前验证名称不为空,确保模板有有效的标识。controller.createTemplate方法接收三个参数,将用户输入的信息保存为新模板。这种验证机制防止了无效数据的产生。
完成对话框的反馈处理:
Navigator.pop(context);
Get.snackbar('成功', '模板创建成功');
}
},
child: const Text('创建'),
),
],
),
),
);
}
创建成功后关闭对话框并显示成功提示。Get.snackbar提供轻量级的反馈信息,不会打断用户的操作流程。这种即时反馈让用户明确知道操作已完成。如果名称为空,按钮点击不会有任何效果,这是一种隐式的错误处理方式。更完善的实现可以添加明确的错误提示。
编辑模板对话框
初始化编辑对话框:
void _showEditTemplateDialog(BuildContext context, NoteController controller, NoteTemplate template) {
final nameController = TextEditingController(text: template.name);
final contentController = TextEditingController(text: template.content);
TemplateType selectedType = template.type;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('编辑模板'),
_showEditTemplateDialog与创建对话框类似,但预填充了现有模板的数据。TextEditingController的构造函数接收text参数,将现有内容显示在输入框中。selectedType初始化为模板当前的类型。这种设计让用户可以在现有基础上修改,而不需要重新输入所有信息。预填充是编辑功能的关键特性,大大提升了用户体验。
构建编辑对话框的内容:
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: '模板名称',
border: OutlineInputBorder(),
),
),
编辑对话框的内容结构与创建对话框完全相同,保持了界面的一致性。用户在两种场景下看到相同的布局和交互方式,降低了学习成本。TextField显示预填充的名称,用户可以直接修改。这种一致性设计是优秀用户体验的重要组成部分,让用户能够快速掌握应用的使用方法。
添加类型选择和内容输入:
SizedBox(height: 16.h),
Text(
'模板类型',
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500),
),
SizedBox(height: 8.h),
TemplateTypeSelector(
selectedType: selectedType,
onTypeChanged: (type) => setState(() => selectedType = type),
),
SizedBox(height: 16.h),
类型选择器显示当前模板的类型,用户可以更改。间距设置与创建对话框保持一致,确保视觉和谐。TemplateTypeSelector组件的复用体现了组件化设计的优势,相同的代码在不同场景下都能正常工作。setState确保类型选择的变化能够实时反映在界面上。
完成内容输入区域:
TextField(
controller: contentController,
maxLines: 5,
decoration: const InputDecoration(
labelText: '模板内容',
border: OutlineInputBorder(),
),
),
],
),
),
内容输入框同样预填充了现有内容,用户可以修改模板的预置文本。maxLines设置为5行,提供足够的编辑空间。OutlineInputBorder保持了统一的视觉风格。这种设计让编辑操作变得直观简单,用户可以看到当前的内容并进行修改,而不需要记忆原有内容。
编辑对话框的保存操作
实现保存按钮的逻辑:
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
if (nameController.text.isNotEmpty) {
controller.updateTemplate(
template.copyWith(
name: nameController.text,
actions区域的结构与创建对话框相同,但确认按钮的文字改为"保存"。保存操作使用updateTemplate方法而不是createTemplate。copyWith方法创建模板的副本,只更新修改的字段,保持其他字段不变。这种不可变数据模式是Flutter推荐的做法,避免了意外的副作用。
完成更新操作:
content: contentController.text,
type: selectedType,
),
);
Navigator.pop(context);
Get.snackbar('成功', '模板更新成功');
}
},
child: const Text('保存'),
),
],
),
),
);
}
copyWith方法传入新的name、content和type值,创建更新后的模板对象。更新成功后关闭对话框并显示成功提示。"模板更新成功"的消息明确告知用户操作结果。这种清晰的反馈机制让用户对系统状态有明确的认知。如果未来需要添加更多字段,copyWith模式可以轻松扩展。
删除确认对话框
实现删除前的确认机制:
void _confirmDeleteTemplate(BuildContext context, NoteController controller, NoteTemplate template) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除模板'),
content: Text('确定要删除模板"${template.name}"吗?\n此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
_confirmDeleteTemplate显示删除确认对话框,这是防止误操作的重要保护机制。content明确显示要删除的模板名称,并警告操作不可恢复。这种明确的提示让用户在执行危险操作前有机会重新考虑。取消按钮提供了退出的途径,避免了不必要的删除。确认对话框是用户体验设计中的最佳实践。
添加删除确认按钮:
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
controller.deleteTemplate(template.id);
Navigator.pop(context);
Get.snackbar('成功', '模板已删除');
},
child: const Text('删除'),
),
],
),
);
}
删除按钮使用红色背景,视觉上强调这是危险操作。点击后调用deleteTemplate方法执行删除,然后关闭对话框并显示成功提示。红色按钮是通用的危险操作标识,用户能够直观理解其含义。这种设计在保护用户数据的同时,也不会过度阻碍正常的删除操作。
搜索栏的实现
创建搜索输入框:
Widget _buildSearchBar() {
return Padding(
padding: EdgeInsets.all(12.w),
child: TextField(
onChanged: (value) => _searchTemplates(value),
decoration: InputDecoration(
hintText: '搜索模板...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
),
_buildSearchBar创建搜索输入框组件。onChanged回调在用户输入时触发搜索。prefixIcon显示搜索图标,增强视觉识别性。圆角边框(25的半径)使搜索框呈现胶囊形状,这是现代应用中常见的搜索框设计。hintText提供输入提示,引导用户使用搜索功能。
完成搜索框样式:
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
),
),
);
}
contentPadding设置输入框内部的填充,确保文字不会紧贴边缘。水平16.w和垂直12.h的填充提供了舒适的输入空间。Padding组件为整个搜索框添加外边距,使其与其他内容保持适当距离。这些细节的处理让搜索框既美观又实用,符合用户的使用习惯。
搜索逻辑的实现
实现模板搜索功能:
List<NoteTemplate> _searchTemplates(String query) {
final controller = Get.find<NoteController>();
final allTemplates = controller.templates;
if (query.isEmpty) return allTemplates;
final lowerQuery = query.toLowerCase();
return allTemplates.where((template) =>
template.name.toLowerCase().contains(lowerQuery) ||
_searchTemplates方法实现搜索逻辑。首先获取所有模板,如果查询为空则返回全部。将查询转换为小写实现不区分大小写的搜索。使用where方法过滤模板列表,检查名称是否包含查询字符串。这种模糊搜索方式比精确匹配更加用户友好,用户只需输入部分关键词即可找到目标模板。
完成多字段搜索:
template.content.toLowerCase().contains(lowerQuery) ||
template.type.displayName.toLowerCase().contains(lowerQuery)
).toList();
}
搜索不仅匹配模板名称,还匹配内容和类型名称。这种多字段搜索提高了查找的成功率,用户可以通过任何相关信息找到模板。例如,搜索"会议"可以找到类型为"会议记录"的所有模板。toList()将过滤结果转换为列表。这种全面的搜索策略大大提升了功能的实用性。
排序选项的定义
定义排序方式枚举:
enum TemplateSortOption {
name('名称'),
usageCount('使用次数'),
createdAt('创建时间'),
updatedAt('更新时间');
const TemplateSortOption(this.displayName);
final String displayName;
}
TemplateSortOption枚举定义了四种排序方式。每种方式都有对应的显示名称,用于界面展示。名称排序便于按字母顺序查找,使用次数排序突出热门模板,时间排序帮助用户找到最新或最早的模板。这种多样化的排序选项满足了不同用户的需求,提高了模板管理的灵活性。
排序控制组件
创建排序菜单的基础结构:
class TemplateSortControl extends StatelessWidget {
final TemplateSortOption selectedOption;
final Function(TemplateSortOption) onOptionChanged;
const TemplateSortControl({
super.key,
required this.selectedOption,
required this.onOptionChanged,
});
Widget build(BuildContext context) {
TemplateSortControl是排序控制组件,接收当前选中的排序选项和变化回调。这种设计让组件保持无状态,由父组件控制排序逻辑。required关键字确保必要参数不会缺失。组件化的设计使排序功能可以轻松集成到不同的页面中,提高了代码的复用性。
实现排序菜单:
return PopupMenuButton<TemplateSortOption>(
icon: const Icon(Icons.sort),
tooltip: '排序方式',
onSelected: onOptionChanged,
itemBuilder: (context) => TemplateSortOption.values.map((option) =>
PopupMenuItem(
value: option,
child: Row(
children: [
Icon(
_getSortIcon(option),
PopupMenuButton创建下拉菜单,点击排序图标时显示。tooltip提供悬停提示。onSelected回调在用户选择选项时触发。itemBuilder动态生成菜单项,遍历所有排序选项。每个菜单项使用Row布局,包含图标和文字。_getSortIcon方法为不同排序方式提供对应的图标,增强视觉识别性。
完成菜单项的内容:
size: 16.sp,
),
SizedBox(width: 8.w),
Text(option.displayName),
if (selectedOption == option) ...[
const Spacer(),
const Icon(Icons.check, color: Colors.blue),
],
],
),
),
).toList(),
);
}
图标大小设置为16.sp,与文字协调。SizedBox提供图标和文字之间的间距。如果选项被选中,使用Spacer推开后续内容,在右侧显示勾选图标。蓝色的勾选图标清晰地标识当前选中的排序方式。这种视觉反馈让用户明确知道当前使用的排序规则,避免了混淆。
实现排序图标映射:
IconData _getSortIcon(TemplateSortOption option) {
switch (option) {
case TemplateSortOption.name:
return Icons.sort_by_alpha;
case TemplateSortOption.usageCount:
return Icons.trending_up;
case TemplateSortOption.createdAt:
return Icons.schedule;
case TemplateSortOption.updatedAt:
return Icons.update;
}
}
}
_getSortIcon方法为每种排序选项返回对应的图标。sort_by_alpha表示字母排序,trending_up表示使用次数,schedule表示创建时间,update表示更新时间。这些图标直观地传达了排序方式的含义,即使不看文字也能理解。图标的使用提升了界面的国际化友好性,减少了对文字的依赖。
统计信息卡片
构建统计数据的计算逻辑:
Widget _buildStatisticsCard(NoteController controller) {
final templates = controller.templates;
final totalCount = templates.length;
final totalUsage = templates.fold(0, (sum, template) => sum + template.usageCount);
final mostUsedTemplate = templates.isNotEmpty
? templates.reduce((a, b) => a.usageCount > b.usageCount ? a : b)
: null;
return Card(
_buildStatisticsCard方法创建统计信息卡片。totalCount直接获取模板数量。fold方法累加所有模板的使用次数得到总使用次数。reduce方法找出使用次数最多的模板,如果列表为空则返回null。这些统计数据为用户提供了模板使用情况的全局视图,帮助用户了解哪些模板最受欢迎。
构建统计卡片的布局:
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'统计信息',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 12.h),
Card组件提供卡片容器,Padding添加内边距。Column布局垂直排列统计项,crossAxisAlignment.start使内容左对齐。标题使用粗体和较大字号突出显示。SizedBox提供标题和内容之间的间距。这种清晰的层次结构让统计信息易于阅读和理解。
添加统计项:
_buildStatItem('模板总数', '$totalCount'),
_buildStatItem('总使用次数', '$totalUsage'),
if (mostUsedTemplate != null)
_buildStatItem('最常用模板', mostUsedTemplate.name),
],
),
),
);
}
_buildStatItem方法创建单个统计项,传入标签和值。模板总数和总使用次数始终显示,最常用模板只在有模板时显示。这种条件渲染避免了空数据时的显示问题。统计信息的展示让用户对模板的整体使用情况有清晰的认知,也可以作为优化模板管理的依据。
统计项的构建
实现单个统计项的显示:
Widget _buildStatItem(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
children: [
Text(
'$label: ',
style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade600),
),
Text(
value,
_buildStatItem方法创建统计项的UI。Padding提供垂直间距,让各项之间有适当的留白。Row布局水平排列标签和值。标签使用灰色显示,表明这是描述性文字。冒号和空格分隔标签和值,符合常见的键值对显示习惯。这种简洁的设计让统计信息一目了然。
完成统计项样式:
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500),
),
],
),
);
}
值使用中等字重显示,与标签形成对比,突出重要信息。相同的字号保持了视觉统一性。这种标签-值的对比设计是信息展示的经典模式,用户可以快速扫描并获取关键数据。简单的样式设置就能实现清晰的信息层次。
模板导出功能
实现模板导出逻辑:
void _exportTemplates(NoteController controller) async {
final templates = controller.templates;
if (templates.isEmpty) {
Get.snackbar('提示', '没有可导出的模板');
return;
}
final exportData = _generateTemplateExport(templates);
final fileName = 'templates_export_${DateTime.now().millisecondsSinceEpoch}.json';
_exportTemplates方法实现模板导出功能。首先检查是否有模板可导出,避免导出空文件。_generateTemplateExport方法将模板转换为JSON格式。文件名包含时间戳,确保每次导出的文件名唯一,避免覆盖。这种设计让用户可以保留多个导出版本,便于版本管理和回溯。
完成文件写入操作:
try {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$fileName');
await file.writeAsString(exportData);
Get.snackbar('成功', '模板已导出到 $fileName');
} catch (e) {
Get.snackbar('错误', '导出失败:$e');
}
}
使用getApplicationDocumentsDirectory获取应用文档目录,这是存储用户数据的标准位置。File.writeAsString将JSON数据写入文件。try-catch捕获可能的异常,如权限问题或存储空间不足。成功时显示文件名,失败时显示错误信息。这种完善的错误处理确保了功能的健壮性,用户能够清楚地了解操作结果。
模板导入功能
实现文件选择和读取:
void _importTemplates(NoteController controller) async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
if (result != null && result.files.isNotEmpty) {
final file = File(result.files.first.path!);
final content = await file.readAsString();
_importTemplates方法实现模板导入功能。FilePicker.platform.pickFiles打开文件选择器,限制只能选择JSON文件。这种类型限制防止了用户选择无效文件。检查result不为空且包含文件,确保用户确实选择了文件。读取文件内容为字符串,准备解析。文件选择器是跨平台的标准方式,提供了一致的用户体验。
解析并导入模板数据:
final templates = _parseTemplateImport(content);
for (var template in templates) {
controller.createTemplate(
name: template['name'],
content: template['content'],
type: TemplateType.values.firstWhere(
(type) => type.name == template['type'],
orElse: () => TemplateType.custom,
),
);
}
_parseTemplateImport方法解析JSON内容为模板列表。遍历解析结果,为每个模板调用createTemplate方法。使用firstWhere查找匹配的模板类型,如果找不到则使用custom作为默认值。这种容错机制确保了即使导入的数据包含未知类型,也能正常处理。orElse参数提供了优雅的降级方案。
完成导入反馈:
Get.snackbar('成功', '已导入 ${templates.length} 个模板');
}
} catch (e) {
Get.snackbar('错误', '导入失败:$e');
}
}
导入成功后显示导入的模板数量,让用户了解操作结果。catch块捕获所有可能的异常,包括文件读取错误、JSON解析错误等。错误消息包含异常详情,便于问题诊断。这种全面的错误处理确保了功能的稳定性,即使遇到问题也能给用户明确的反馈。
模板数据的JSON转换
实现模板导出的数据生成:
String _generateTemplateExport(List<NoteTemplate> templates) {
final exportList = templates.map((template) => {
'id': template.id,
'name': template.name,
'content': template.content,
'type': template.type.name,
'usageCount': template.usageCount,
'createdAt': template.createdAt.toIso8601String(),
'updatedAt': template.updatedAt.toIso8601String(),
}).toList();
_generateTemplateExport方法将模板列表转换为可导出的JSON格式。使用map方法遍历每个模板,提取关键字段。type.name获取枚举的字符串表示,便于序列化。日期使用ISO 8601格式,这是标准的日期时间表示方式,确保了跨平台兼容性。包含所有必要字段确保导入时能完整恢复模板信息。
完成JSON编码:
return jsonEncode({
'version': '1.0',
'exportDate': DateTime.now().toIso8601String(),
'templates': exportList,
});
}
最终的JSON结构包含版本号、导出日期和模板列表。版本号用于未来的格式兼容性处理,当数据结构变化时可以根据版本号进行相应的转换。导出日期记录了导出时间,便于用户管理多个导出文件。jsonEncode将Map转换为JSON字符串。这种结构化的数据格式既便于机器处理,也便于人工查看。
模板导入的数据解析
实现JSON数据的解析:
List<Map<String, dynamic>> _parseTemplateImport(String content) {
final data = jsonDecode(content) as Map<String, dynamic>;
final version = data['version'] as String?;
if (version != '1.0') {
throw Exception('不支持的模板文件版本');
}
final templatesList = data['templates'] as List<dynamic>;
return templatesList.map((item) => item as Map<String, dynamic>).toList();
}
_parseTemplateImport方法解析导入的JSON数据。首先解码JSON字符串为Map对象。检查版本号,确保文件格式兼容。如果版本不匹配,抛出异常阻止导入。提取templates数组并转换为Map列表。这种版本检查机制为未来的格式升级预留了空间,确保了长期的数据兼容性。
模板的批量操作
实现批量选择模式:
class TemplatesPage extends StatefulWidget {
const TemplatesPage({super.key});
State<TemplatesPage> createState() => _TemplatesPageState();
}
class _TemplatesPageState extends State<TemplatesPage> {
bool _isSelectionMode = false;
final Set<String> _selectedIds = {};
将TemplatesPage改为StatefulWidget以支持批量操作。_isSelectionMode标记是否处于选择模式。_selectedIds存储选中的模板ID集合,使用Set确保ID唯一性。这种设计允许用户同时操作多个模板,提高了管理效率。批量操作是处理大量数据时的必备功能,显著提升了用户体验。
添加批量操作的AppBar:
AppBar _buildAppBar(NoteController controller) {
if (_isSelectionMode) {
return AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => setState(() {
_isSelectionMode = false;
_selectedIds.clear();
}),
),
title: Text('已选择 ${_selectedIds.length} 项'),
_buildAppBar方法根据选择模式返回不同的AppBar。选择模式下,leading显示关闭按钮退出选择模式。title显示选中项数量,让用户清楚当前选择状态。关闭时清空选中集合,重置状态。这种动态AppBar设计是批量操作的标准模式,用户能够直观地理解当前的操作上下文。
完成批量操作按钮:
actions: [
IconButton(
icon: const Icon(Icons.select_all),
onPressed: () => setState(() {
_selectedIds.addAll(controller.templates.map((t) => t.id));
}),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _batchDeleteTemplates(controller),
),
],
);
}
actions包含全选和删除按钮。全选按钮将所有模板ID添加到选中集合。删除按钮触发批量删除操作。这些操作只在选择模式下可用,避免了误操作。全选功能让用户可以快速选择所有项,而不需要逐个点击。批量删除前应该有确认对话框,确保用户了解操作的影响。
返回普通模式的AppBar:
return AppBar(
title: const Text('笔记模板'),
actions: [
IconButton(
icon: const Icon(Icons.checklist),
onPressed: () => setState(() => _isSelectionMode = true),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showCreateTemplateDialog(context, controller),
),
],
);
}
普通模式下的AppBar包含进入选择模式的按钮和添加按钮。checklist图标表示批量选择功能。点击后进入选择模式,用户可以开始选择模板。这种模式切换设计清晰地分离了浏览和批量操作两种场景,避免了界面混乱。用户可以根据需要在两种模式间自由切换。
可选择的模板卡片
实现支持选择的卡片:
Widget _buildSelectableCard(NoteTemplate template, NoteController controller) {
final isSelected = _selectedIds.contains(template.id);
return Card(
margin: EdgeInsets.only(bottom: 8.h),
color: isSelected ? Colors.blue.withOpacity(0.1) : null,
child: ListTile(
leading: _isSelectionMode
? Checkbox(
value: isSelected,
onChanged: (value) => _toggleSelection(template.id),
)
_buildSelectableCard方法创建支持选择的卡片。isSelected判断当前模板是否被选中。选中时卡片背景变为淡蓝色,提供视觉反馈。选择模式下,leading显示复选框而不是图标。Checkbox的状态与_selectedIds同步。这种设计让选择状态清晰可见,用户能够准确地控制选择范围。
完成卡片的点击处理:
: CircleAvatar(
backgroundColor: _getTemplateColor(template.type).withOpacity(0.1),
child: Icon(
_getTemplateIcon(template.type),
color: _getTemplateColor(template.type),
),
),
title: Text(template.name),
onTap: () => _isSelectionMode
? _toggleSelection(template.id)
: _useTemplate(template, controller),
),
);
}
非选择模式下显示类型图标。onTap根据当前模式执行不同操作:选择模式下切换选中状态,普通模式下使用模板创建笔记。这种上下文相关的交互设计让同一个手势在不同场景下有不同的含义,提高了界面的空间利用率。用户能够根据当前模式自然地理解点击的作用。
选择状态的切换
实现选择切换逻辑:
void _toggleSelection(String templateId) {
setState(() {
if (_selectedIds.contains(templateId)) {
_selectedIds.remove(templateId);
} else {
_selectedIds.add(templateId);
}
});
}
_toggleSelection方法切换模板的选中状态。如果已选中则移除,未选中则添加。使用setState触发界面更新,确保复选框和背景色同步变化。这种简单的切换逻辑是多选功能的核心,通过Set的特性自动处理了重复添加的问题。
批量删除操作
实现批量删除确认:
void _batchDeleteTemplates(NoteController controller) {
if (_selectedIds.isEmpty) {
Get.snackbar('提示', '请先选择要删除的模板');
return;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('批量删除'),
content: Text('确定要删除选中的 ${_selectedIds.length} 个模板吗?\n此操作不可恢复。'),
_batchDeleteTemplates方法处理批量删除。首先检查是否有选中项,避免空操作。显示确认对话框,明确告知删除数量和不可恢复性。这种明确的提示是批量操作的重要保护机制,防止用户误删大量数据。数量的显示让用户能够判断选择是否正确。
完成批量删除执行:
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
for (var id in _selectedIds) {
controller.deleteTemplate(id);
}
setState(() {
_selectedIds.clear();
_isSelectionMode = false;
});
Navigator.pop(context);
Get.snackbar('成功', '已删除选中的模板');
},
child: const Text('删除'),
),
],
),
);
}
确认后遍历选中的ID,逐个调用deleteTemplate删除。删除完成后清空选中集合并退出选择模式。关闭对话框并显示成功提示。红色删除按钮强调操作的危险性。这种批量处理方式简化了代码逻辑,虽然不是最优性能方案,但对于模板数量不大的场景足够使用。
模板的拖拽排序
实现拖拽排序功能:
Widget _buildReorderableList(NoteController controller) {
return ReorderableListView.builder(
padding: EdgeInsets.all(12.w),
itemCount: controller.templates.length,
onReorder: (oldIndex, newIndex) {
controller.reorderTemplates(oldIndex, newIndex);
},
itemBuilder: (context, index) {
final template = controller.templates[index];
return _buildDraggableCard(template, controller);
},
);
}
ReorderableListView.builder创建支持拖拽排序的列表。onReorder回调在用户完成拖拽时触发,传入旧位置和新位置索引。controller.reorderTemplates方法处理实际的排序逻辑。这种拖拽排序功能让用户可以自定义模板的显示顺序,将常用模板放在前面,提高了使用效率。
创建可拖拽的卡片:
Widget _buildDraggableCard(NoteTemplate template, NoteController controller) {
return Card(
key: ValueKey(template.id),
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
leading: Icon(Icons.drag_handle),
title: Text(template.name),
subtitle: Text(template.type.displayName),
trailing: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () => _showTemplateOptions(template, controller),
),
),
);
}
_buildDraggableCard创建可拖拽的卡片,必须提供唯一的key。leading显示拖拽手柄图标,提示用户可以拖动。subtitle显示模板类型,trailing提供更多操作选项。ValueKey使用模板ID确保每个卡片的唯一性,这对拖拽排序的正确性至关重要。拖拽手柄的视觉提示让功能的可发现性更好。
模板的收藏功能
添加收藏标记:
class NoteTemplate {
final String id;
final String name;
final String content;
final TemplateType type;
final int usageCount;
final bool isFavorite;
final DateTime createdAt;
final DateTime updatedAt;
const NoteTemplate({
required this.id,
required this.name,
在NoteTemplate模型中添加isFavorite字段,标记模板是否被收藏。收藏功能让用户可以快速访问常用模板,不需要在长列表中查找。这个布尔字段的添加不会显著增加数据存储开销,但能大幅提升用户体验。收藏是现代应用的标准功能,用户对这个概念非常熟悉。
实现收藏切换按钮:
Widget _buildFavoriteButton(NoteTemplate template, NoteController controller) {
return IconButton(
icon: Icon(
template.isFavorite ? Icons.star : Icons.star_border,
color: template.isFavorite ? Colors.amber : null,
),
onPressed: () {
controller.toggleTemplateFavorite(template.id);
},
);
}
_buildFavoriteButton创建收藏按钮。收藏状态下显示实心星星和琥珀色,未收藏显示空心星星。点击切换收藏状态。星星图标是收藏功能的通用标识,用户无需学习即可理解。琥珀色的选择符合星星的自然颜色,视觉效果友好。这种即时反馈的交互让收藏操作变得轻松愉快。
收藏模板的筛选
实现收藏筛选功能:
Widget _buildFilterChips(NoteController controller) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
child: Row(
children: [
FilterChip(
label: const Text('全部'),
selected: _filterMode == FilterMode.all,
onSelected: (selected) => setState(() => _filterMode = FilterMode.all),
),
SizedBox(width: 8.w),
FilterChip(
label: const Text('收藏'),
_buildFilterChips创建筛选芯片组。FilterChip提供可选择的筛选选项。"全部"和"收藏"两个选项让用户可以快速切换视图。selected属性控制芯片的选中状态。这种筛选设计让用户可以专注于收藏的模板,减少了视觉干扰。FilterChip是Material Design中的标准组件,提供了良好的交互体验。
完成筛选逻辑:
selected: _filterMode == FilterMode.favorite,
onSelected: (selected) => setState(() => _filterMode = FilterMode.favorite),
),
],
),
);
}
List<NoteTemplate> _getFilteredTemplates(NoteController controller) {
final templates = controller.templates;
if (_filterMode == FilterMode.favorite) {
return templates.where((t) => t.isFavorite).toList();
}
return templates;
}
_getFilteredTemplates方法根据筛选模式返回相应的模板列表。收藏模式下只返回isFavorite为true的模板。这种筛选逻辑简单高效,不需要复杂的查询。筛选功能与搜索功能可以组合使用,提供更强大的查找能力。用户可以先筛选收藏,再在收藏中搜索,逐步缩小范围。
模板的预览功能
实现模板预览对话框:
void _showTemplatePreview(BuildContext context, NoteTemplate template) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(template.name),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildPreviewInfo('类型', template.type.displayName),
_showTemplatePreview方法显示模板的详细预览。AlertDialog提供对话框容器,title显示模板名称。SingleChildScrollView确保内容过长时可以滚动。Column布局垂直排列预览信息。_buildPreviewInfo方法创建信息行。预览功能让用户在使用模板前了解其完整内容,避免了创建后发现不合适的情况。
添加预览内容:
_buildPreviewInfo('使用次数', '${template.usageCount} 次'),
_buildPreviewInfo('创建时间', _formatDate(template.createdAt)),
SizedBox(height: 16.h),
Text(
'模板内容',
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500),
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
预览信息包括类型、使用次数和创建时间。模板内容单独显示,使用Container提供背景和边框。这种分层展示让信息结构清晰。使用次数和创建时间帮助用户判断模板的可靠性和时效性。格式化的日期显示更加友好,用户可以快速理解时间信息。
完成预览内容样式:
color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.withOpacity(0.3)),
),
child: Text(
template.content.isEmpty ? '空白模板' : template.content,
style: TextStyle(fontSize: 13.sp),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_useTemplate(template, controller);
},
child: const Text('使用此模板'),
),
],
),
);
}
内容区域使用淡灰色背景和边框,与周围内容区分。空白模板显示提示文字。actions包含关闭和使用按钮,用户可以直接从预览对话框使用模板。这种一站式的预览和使用流程减少了操作步骤,提高了效率。预览对话框提供了完整的信息,帮助用户做出明智的选择。
模板的复制功能
实现模板复制:
void _duplicateTemplate(NoteTemplate template, NoteController controller) {
final newTemplate = template.copyWith(
id: _generateId(),
name: '${template.name} (副本)',
usageCount: 0,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
controller.addTemplate(newTemplate);
Get.snackbar('成功', '模板已复制');
}
_duplicateTemplate方法复制现有模板。使用copyWith创建副本,生成新ID,名称添加"(副本)"后缀,重置使用次数和时间戳。这种复制功能让用户可以基于现有模板创建变体,而不需要从头开始。重置使用次数确保了统计的准确性。名称后缀清楚地标识了复制关系,避免了混淆。
模板的分享功能
实现模板分享:
void _shareTemplate(NoteTemplate template) async {
final shareData = jsonEncode({
'name': template.name,
'content': template.content,
'type': template.type.name,
});
await Share.share(
shareData,
subject: '分享模板:${template.name}',
);
}
_shareTemplate方法将模板转换为JSON格式并分享。只包含必要的字段(名称、内容、类型),不包含ID和统计信息。Share.share调用系统分享功能,用户可以通过各种方式发送模板。subject提供分享标题。这种分享功能让用户可以与他人交流模板,促进了模板的传播和改进。
模板的使用统计
实现使用次数的更新:
void _useTemplate(NoteTemplate template, NoteController controller) {
final note = controller.createNote(templateId: template.id);
controller.updateTemplate(
template.copyWith(
usageCount: template.usageCount + 1,
updatedAt: DateTime.now(),
),
);
Get.to(() => NoteEditorPage(note: note));
}
_useTemplate方法在使用模板时自动增加使用次数。创建笔记后立即更新模板的usageCount和updatedAt。这种自动统计让使用数据始终保持准确,无需用户手动操作。使用次数是评估模板价值的重要指标,可以帮助用户识别最实用的模板。更新时间的记录也有助于了解模板的活跃度。
空状态的处理
实现空列表提示:
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.description_outlined,
size: 80.sp,
color: Colors.grey.shade300,
),
SizedBox(height: 16.h),
Text(
'还没有模板',
style: TextStyle(
fontSize: 18.sp,
_buildEmptyState方法创建空状态界面。Center居中显示内容,Column垂直排列图标和文字。大尺寸的淡灰色图标提供视觉焦点。这种空状态设计让用户明确知道列表为空,而不是加载失败或其他问题。友好的空状态提示是良好用户体验的重要组成部分。
完成空状态提示:
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
SizedBox(height: 8.h),
Text(
'点击右上角的 + 按钮创建第一个模板',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade500,
),
),
],
),
);
}
文字提示包含标题和操作指引。标题说明当前状态,指引告诉用户如何创建模板。这种引导性的空状态设计帮助新用户快速上手,减少了困惑。灰色的配色方案表明这是辅助信息,不会过于突出。清晰的行动指引降低了用户的学习成本。
加载状态的处理
实现加载指示器:
Widget _buildLoadingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16.h),
Text(
'加载中...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
],
),
);
}
_buildLoadingState方法创建加载状态界面。CircularProgressIndicator显示旋转的加载动画。配合文字提示,让用户知道系统正在处理。这种加载状态在数据从网络或数据库加载时显示,避免了界面的突然变化。虽然本地数据加载通常很快,但提供加载状态仍然是好的实践,确保了各种情况下的用户体验。
错误状态的处理
实现错误提示界面:
Widget _buildErrorState(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 80.sp,
color: Colors.red.shade300,
),
SizedBox(height: 16.h),
Text(
'加载失败',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
_buildErrorState方法创建错误状态界面。error_outline图标使用红色,清楚地表明出现了问题。"加载失败"标题直接说明状态。这种明确的错误提示让用户了解当前情况,而不是让他们猜测发生了什么。红色是错误的通用标识色,用户能够立即识别问题的严重性。
添加重试按钮:
color: Colors.grey.shade600,
),
),
SizedBox(height: 8.h),
Text(
error,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 24.h),
ElevatedButton.icon(
onPressed: () => _retryLoading(),
icon: const Icon(Icons.refresh),
label: const Text('重试'),
),
],
),
);
}
显示具体的错误信息,帮助用户或开发者诊断问题。重试按钮让用户可以尝试重新加载,而不需要退出页面。ElevatedButton.icon组合图标和文字,提供清晰的操作提示。这种错误处理机制提供了恢复路径,避免了用户陷入死胡同。即使出现错误,用户也能通过重试继续使用应用。
下拉刷新功能
实现下拉刷新:
Widget _buildRefreshableList(NoteController controller) {
return RefreshIndicator(
onRefresh: () async {
await controller.refreshTemplates();
},
child: ListView.builder(
padding: EdgeInsets.all(12.w),
itemCount: controller.templates.length,
itemBuilder: (context, index) {
final template = controller.templates[index];
return _buildTemplateCard(template, controller);
},
),
);
}
RefreshIndicator包裹ListView实现下拉刷新功能。onRefresh回调执行刷新逻辑,返回Future表示异步操作。用户下拉列表时触发刷新,加载最新数据。这种刷新机制是移动应用的标准交互,用户非常熟悉。即使数据存储在本地,刷新功能仍然有用,可以重新加载数据或同步云端更新。
模板的分组显示
实现按类型分组:
Widget _buildGroupedList(NoteController controller) {
final groupedTemplates = _groupTemplatesByType(controller.templates);
return ListView.builder(
padding: EdgeInsets.all(12.w),
itemCount: groupedTemplates.length,
itemBuilder: (context, index) {
final entry = groupedTemplates.entries.elementAt(index);
return _buildTypeGroup(entry.key, entry.value, controller);
},
);
}
Map<TemplateType, List<NoteTemplate>> _groupTemplatesByType(List<NoteTemplate> templates) {
_buildGroupedList方法创建分组列表。_groupTemplatesByType将模板按类型分组,返回Map结构。ListView.builder遍历分组,为每个类型创建一个组。这种分组显示让相同类型的模板聚集在一起,用户可以快速找到特定类型的模板。分组是组织大量数据的有效方式,提高了信息的可读性。
完成分组逻辑:
final Map<TemplateType, List<NoteTemplate>> grouped = {};
for (var template in templates) {
grouped.putIfAbsent(template.type, () => []).add(template);
}
return grouped;
}
遍历所有模板,使用putIfAbsent确保每个类型都有对应的列表。将模板添加到对应类型的列表中。这种分组算法简单高效,时间复杂度为O(n)。Map的使用让查找和插入操作都很快。分组后的数据结构便于渲染和管理,也便于实现折叠展开等高级功能。
分组标题的设计
实现类型组的标题:
Widget _buildTypeGroup(TemplateType type, List<NoteTemplate> templates, NoteController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
children: [
Icon(type.icon, color: type.color, size: 20.sp),
SizedBox(width: 8.w),
Text(
type.displayName,
_buildTypeGroup方法创建类型组的UI。Column垂直排列标题和模板列表。标题行包含类型图标、名称和模板数量。图标使用类型的主题色,增强视觉识别。这种带图标的标题设计让分组更加清晰,用户可以快速扫描并定位目标类型。
完成分组标题和内容:
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: type.color,
),
),
SizedBox(width: 4.w),
Text(
'(${templates.length})',
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
],
),
),
...templates.map((template) => _buildTemplateCard(template, controller)),
SizedBox(height: 16.h),
],
);
}
类型名称使用粗体和主题色显示,突出分组标识。括号中显示该类型的模板数量,让用户了解每个类型的规模。使用展开运算符将模板列表展开为多个卡片。底部添加间距分隔不同的组。这种设计清晰地划分了不同类型的模板,同时保持了整体的连贯性。数量统计帮助用户快速评估每个类型的内容丰富度。
总结
模板列表功能的完整实现涵盖了创建、编辑、删除、搜索、排序、统计、导入导出、批量操作、拖拽排序、收藏、预览、复制、分享等多个方面。通过合理的组件设计和状态管理,我们构建了一个功能强大且用户友好的模板管理系统。
每个功能都经过精心设计,考虑了用户体验和代码可维护性。从基础的CRUD操作到高级的批量处理和数据导入导出,系统提供了完整的模板管理能力。视觉设计上使用了Material Design规范,确保了界面的一致性和美观性。
模板系统的核心价值在于提高用户的笔记创建效率。通过预设的内容结构,用户可以快速开始写作,而不需要每次都从空白页面开始。多样化的模板类型满足了不同场景的需求,从会议记录到旅行计划,覆盖了日常生活和工作的各个方面。
搜索和筛选功能让用户能够在大量模板中快速找到需要的内容。排序功能提供了多种视角查看模板,无论是按名称、使用频率还是时间排序,都能满足不同的查找需求。统计信息为用户提供了模板使用情况的全局视图,帮助识别最有价值的模板。
批量操作和拖拽排序等高级功能提升了管理效率,让用户可以轻松组织大量模板。导入导出功能支持数据备份和跨设备同步,保护了用户的数据安全。收藏和分享功能增强了模板的个性化和社交属性,让优秀的模板可以被更多人使用。
通过这个完整的模板列表实现,我们展示了如何在Flutter for OpenHarmony中构建复杂的列表管理功能。代码结构清晰,组件复用性高,易于扩展和维护。这些设计原则和实现技巧可以应用到其他类似的功能开发中,为构建高质量的应用提供参考。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)