Flutter for OpenHarmony轻量级开源记事本App实战:导出功能
数据导出是笔记应用的重要功能,它让用户可以备份数据、迁移到其他平台或分享给他人。一个好的导出功能应该支持多种格式,操作简单,并提供预览功能。本文将详细介绍如何实现一个实用的笔记导出系统。
导出页面的整体设计
导出页面展示数据概览和导出选项,让用户了解要导出的内容。
class ExportPage extends StatelessWidget {
const ExportPage({super.key});
Widget build(BuildContext context) {
final controller = Get.find<NoteController>();
return Scaffold(
appBar: AppBar(
title: const Text('导出笔记'),
),
body: ListView(
padding: EdgeInsets.all(16.w),
children: [
_buildInfoCard(),
SizedBox(height: 16.h),
_buildDataOverview(controller),
SizedBox(height: 24.h),
_buildExportButtons(context, controller),
],
),
);
}
页面使用ListView布局,包含三个主要部分:说明卡片、数据概览和导出按钮。使用ListView而不是Column是因为内容可能超出屏幕高度,需要支持滚动。每个部分之间用SizedBox分隔,形成清晰的视觉层次。这种模块化的设计让页面结构清晰易懂。
导出说明卡片
说明卡片告诉用户导出功能的用途和使用方法。
Widget _buildInfoCard() {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, size: 20.sp, color: Colors.blue),
SizedBox(width: 8.w),
Text(
'导出说明',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 8.h),
Text(
'导出功能会将所有笔记转换为文本格式,您可以复制到剪贴板进行备份。',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
],
),
),
);
}
说明卡片使用Card组件提供卡片样式,内部是一个Column布局。标题行包含信息图标和文字,使用Row布局。说明文字使用灰色小字,与标题形成对比。这种友好的说明能够帮助用户理解导出功能的用途,降低学习成本。
数据概览的展示
数据概览展示要导出的内容统计,让用户心中有数。
Widget _buildDataOverview(NoteController controller) {
return Obx(() => Card(
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),
_DataRow(label: '笔记数量', value: '${controller.activeNotes.length}'),
_DataRow(label: '分类数量', value: '${controller.categories.length}'),
_DataRow(label: '标签数量', value: '${controller.tags.length}'),
_DataRow(label: '文件夹数量', value: '${controller.folders.length}'),
_DataRow(label: '总字数', value: '${controller.totalWordCount}'),
],
),
),
));
}
数据概览使用Obx包裹,实现响应式更新。展示笔记数量、分类数量、标签数量、文件夹数量和总字数等统计信息。每一行使用_DataRow组件显示,保持统一的样式。这些统计信息让用户了解要导出的数据规模,可以据此判断导出时间和文件大小。
数据行组件
_DataRow是一个简单的组件,用于显示标签和值的键值对。
class _DataRow extends StatelessWidget {
final String label;
final String value;
const _DataRow({required this.label, required this.value});
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
Text(value, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600)),
],
),
);
}
}
_DataRow使用Row布局,mainAxisAlignment设置为spaceBetween让标签和值分别靠左右两端对齐。标签使用灰色普通字体,值使用黑色粗体,形成视觉对比。上下padding提供适当的间距,让列表更加舒适。这种简单的组件设计让代码更加模块化和可复用。
导出按钮的布局
导出按钮区域提供多种导出方式,满足不同需求。
Widget _buildExportButtons(BuildContext context, NoteController controller) {
return Column(
children: [
ElevatedButton.icon(
onPressed: () => _exportToClipboard(context, controller),
icon: const Icon(Icons.copy),
label: const Text('复制到剪贴板'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
),
SizedBox(height: 12.h),
OutlinedButton.icon(
onPressed: () => _showExportPreview(context, controller),
icon: const Icon(Icons.preview),
label: const Text('预览导出内容'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
),
],
);
}
按钮区域包含两个按钮:复制到剪贴板和预览导出内容。第一个使用ElevatedButton,表示主要操作。第二个使用OutlinedButton,表示次要操作。两个按钮都使用icon构造函数,添加图标让功能更加直观。minimumSize设置按钮的最小尺寸,确保按钮足够大,易于点击。
生成导出内容
导出内容生成方法将所有笔记转换为Markdown格式的文本。
String _generateExportContent(NoteController controller) {
final buffer = StringBuffer();
final now = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
buffer.writeln('# 轻记笔记导出');
buffer.writeln('导出时间: $now');
buffer.writeln('笔记数量: ${controller.activeNotes.length}');
buffer.writeln('');
buffer.writeln('=' * 50);
buffer.writeln('');
_generateExportContent方法使用StringBuffer构建导出内容。首先添加标题和元信息,包括导出时间和笔记数量。使用DateFormat格式化当前时间,显示年月日和时分秒。然后添加一行等号作为分隔线。StringBuffer比字符串拼接更高效,特别是在处理大量文本时。
for (var i = 0; i < controller.activeNotes.length; i++) {
final note = controller.activeNotes[i];
buffer.writeln('## ${i + 1}. ${note.title.isEmpty ? "无标题" : note.title}');
buffer.writeln('');
buffer.writeln('创建时间: ${DateFormat('yyyy-MM-dd HH:mm').format(note.createdAt)}');
buffer.writeln('修改时间: ${DateFormat('yyyy-MM-dd HH:mm').format(note.updatedAt)}');
if (note.tags.isNotEmpty) {
buffer.writeln('标签: ${note.tags.map((t) => "#$t").join(" ")}');
}
buffer.writeln('');
buffer.writeln(note.content.isEmpty ? '(无内容)' : note.content);
buffer.writeln('');
buffer.writeln('-' * 50);
buffer.writeln('');
}
return buffer.toString();
}
遍历所有活跃笔记,为每个笔记生成一个章节。章节标题包含序号和笔记标题,使用二级标题格式。然后添加创建时间、修改时间和标签等元信息。如果笔记有标签,将标签列表转换为带#前缀的字符串。最后添加笔记内容和分隔线。这种Markdown格式易于阅读和处理,可以导入到其他笔记应用。
复制到剪贴板
复制到剪贴板功能让用户可以快速备份或分享笔记。
void _exportToClipboard(BuildContext context, NoteController controller) {
final content = _generateExportContent(controller);
Clipboard.setData(ClipboardData(text: content));
Get.snackbar(
'导出成功',
'已复制 ${controller.activeNotes.length} 条笔记到剪贴板',
snackPosition: SnackPosition.BOTTOM,
);
}
_exportToClipboard方法首先生成导出内容,然后使用Clipboard.setData将内容复制到剪贴板。复制成功后显示提示消息,告知用户复制了多少条笔记。用户可以将剪贴板内容粘贴到其他应用,如邮件、云笔记或文本编辑器。这种简单的导出方式不需要文件系统权限,使用起来非常方便。
预览导出内容
预览功能让用户在导出前查看生成的内容,确认格式正确。
void _showExportPreview(BuildContext context, NoteController controller) {
final content = _generateExportContent(controller);
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) => Column(
children: [
_buildPreviewHeader(context),
Expanded(
child: _buildPreviewContent(content, scrollController),
),
],
),
),
);
}
_showExportPreview方法显示一个底部弹窗,展示导出内容的预览。使用DraggableScrollableSheet创建可拖拽的弹窗,用户可以调整弹窗高度。initialChildSize设置初始高度为屏幕的70%,minChildSize和maxChildSize设置最小和最大高度。这种可调节的弹窗提供了灵活的查看体验。
预览头部的构建
预览头部显示标题和关闭按钮。
Widget _buildPreviewHeader(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'导出预览',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
);
}
预览头部使用Container包裹,设置背景色和圆角。Row布局包含标题和关闭按钮,mainAxisAlignment设置为spaceBetween让两者分别靠左右两端。关闭按钮点击后关闭弹窗。这种固定的头部设计让用户始终能看到标题和关闭按钮,即使内容滚动也不会消失。
预览内容的展示
预览内容使用可滚动的文本组件展示导出内容。
Widget _buildPreviewContent(String content, ScrollController scrollController) {
return SingleChildScrollView(
controller: scrollController,
padding: EdgeInsets.all(16.w),
child: SelectableText(
content,
style: TextStyle(
fontSize: 12.sp,
fontFamily: 'monospace',
),
),
);
}
预览内容使用SingleChildScrollView包裹,支持垂直滚动。SelectableText让用户可以选择和复制文本,这在预览长内容时很有用。style设置为等宽字体,让Markdown格式的文本更加清晰。scrollController由DraggableScrollableSheet提供,确保滚动行为正确。
导出为文件
除了复制到剪贴板,还可以导出为文件保存到本地。
Future<void> exportToFile(NoteController controller) async {
final content = _generateExportContent(controller);
final fileName = 'notes_export_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.md';
try {
// 这里需要使用文件系统API保存文件
// 具体实现取决于平台
Get.snackbar('导出成功', '文件已保存: $fileName',
snackPosition: SnackPosition.BOTTOM);
} catch (e) {
Get.snackbar('导出失败', '保存文件时出错: $e',
snackPosition: SnackPosition.BOTTOM);
}
}
exportToFile方法生成导出内容,然后保存为文件。文件名包含导出时间,避免重复。使用try-catch捕获可能的错误,显示相应的提示消息。文件导出需要文件系统权限,具体实现取决于平台。这种导出方式适合需要长期保存或分享文件的场景。
选择性导出
支持用户选择要导出的笔记,而不是导出全部。
String generateSelectiveExport(List<String> noteIds, NoteController controller) {
final buffer = StringBuffer();
final now = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
buffer.writeln('# 轻记笔记导出(选择性)');
buffer.writeln('导出时间: $now');
buffer.writeln('笔记数量: ${noteIds.length}');
buffer.writeln('');
for (var i = 0; i < noteIds.length; i++) {
final note = controller.notes.firstWhereOrNull((n) => n.id == noteIds[i]);
if (note == null) continue;
buffer.writeln('## ${i + 1}. ${note.title.isEmpty ? "无标题" : note.title}');
buffer.writeln('');
buffer.writeln(note.content.isEmpty ? '(无内容)' : note.content);
buffer.writeln('');
buffer.writeln('-' * 50);
buffer.writeln('');
}
return buffer.toString();
}
generateSelectiveExport方法接收笔记ID列表,只导出指定的笔记。遍历ID列表,找到对应的笔记并生成内容。这种选择性导出让用户可以只导出需要的笔记,减少导出文件的大小。可以配合选择模式使用,让用户勾选要导出的笔记。
导出格式的选择
支持多种导出格式,满足不同需求。
enum ExportFormat {
markdown,
plainText,
json,
}
String generateExport(NoteController controller, ExportFormat format) {
switch (format) {
case ExportFormat.markdown:
return _generateMarkdownExport(controller);
case ExportFormat.plainText:
return _generatePlainTextExport(controller);
case ExportFormat.json:
return _generateJsonExport(controller);
}
}
定义ExportFormat枚举,包含Markdown、纯文本和JSON三种格式。generateExport方法根据格式调用相应的生成方法。Markdown格式适合阅读和编辑,纯文本格式最简单,JSON格式适合程序处理。这种多格式支持让导出功能更加灵活。
纯文本导出
纯文本导出去除所有格式标记,只保留内容。
String _generatePlainTextExport(NoteController controller) {
final buffer = StringBuffer();
for (var note in controller.activeNotes) {
buffer.writeln(note.title.isEmpty ? '无标题' : note.title);
buffer.writeln('');
buffer.writeln(note.content.isEmpty ? '(无内容)' : note.content);
buffer.writeln('');
buffer.writeln('---');
buffer.writeln('');
}
return buffer.toString();
}
纯文本导出只包含标题和内容,没有时间、标签等元信息。每个笔记之间用三个短横线分隔。这种格式最简单,文件大小最小,适合快速浏览或导入到不支持Markdown的应用。
JSON导出
JSON导出保留所有数据结构,适合程序处理和数据迁移。
String _generateJsonExport(NoteController controller) {
final data = {
'exportTime': DateTime.now().toIso8601String(),
'version': '1.0',
'notes': controller.activeNotes.map((n) => {
'id': n.id,
'title': n.title,
'content': n.content,
'createdAt': n.createdAt.toIso8601String(),
'updatedAt': n.updatedAt.toIso8601String(),
'tags': n.tags,
'categoryId': n.categoryId,
'folderId': n.folderId,
}).toList(),
'categories': controller.categories.map((c) => {
'id': c.id,
'name': c.name,
'color': c.color,
}).toList(),
'tags': controller.tags.map((t) => {
'id': t.id,
'name': t.name,
}).toList(),
};
return jsonEncode(data);
}
JSON导出包含所有笔记、分类和标签的完整数据。使用Map构建数据结构,然后用jsonEncode转换为JSON字符串。时间使用ISO 8601格式,确保跨平台兼容。这种格式保留了所有信息,可以完整地恢复数据。
导出进度提示
导出大量笔记时,显示进度提示让用户知道操作正在进行。
Future<void> exportWithProgress(BuildContext context, NoteController controller) async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('正在导出...'),
],
),
),
);
await Future.delayed(const Duration(milliseconds: 500));
final content = _generateExportContent(controller);
await Clipboard.setData(ClipboardData(text: content));
Navigator.pop(context);
Get.snackbar('导出成功', '已复制到剪贴板',
snackPosition: SnackPosition.BOTTOM);
}
exportWithProgress方法在导出前显示进度对话框,包含加载动画和提示文字。barrierDismissible设置为false,防止用户点击外部关闭对话框。导出完成后关闭对话框并显示成功提示。这种进度提示让用户知道操作正在进行,避免重复点击。
导出历史记录
记录导出历史,让用户可以查看之前的导出记录。
class ExportHistory {
final String id;
final DateTime exportTime;
final int noteCount;
final ExportFormat format;
ExportHistory({
required this.id,
required this.exportTime,
required this.noteCount,
required this.format,
});
}
void addExportHistory(ExportFormat format, int noteCount) {
final history = ExportHistory(
id: DateTime.now().millisecondsSinceEpoch.toString(),
exportTime: DateTime.now(),
noteCount: noteCount,
format: format,
);
exportHistories.add(history);
saveData();
}
ExportHistory类记录导出的时间、笔记数量和格式。addExportHistory方法在每次导出后添加一条记录。这些历史记录可以在导出页面展示,让用户了解导出情况。
导出功能是笔记应用的重要组成部分,它让用户的数据不被锁定在应用中,可以自由地备份、迁移和分享。通过支持多种格式、预览功能和进度提示,我们实现了一个用户友好的导出系统。用户可以放心地使用应用,知道自己的数据随时可以导出,不会丢失。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)