Flutter for OpenHarmony轻量级开源记事本app实战——回收站
本文介绍了记事本应用中回收站功能的实现方法。回收站采用软删除机制,通过标记而非真实删除来保护用户数据。文章详细讲解了回收站页面的设计,包括恢复笔记、永久删除和清空回收站等功能。代码实现上使用了Flutter框架,结合GetX状态管理,通过Obx实现数据响应式更新。页面包含空状态提示、笔记列表展示、删除时间显示以及操作按钮等元素,并强调了危险操作的二次确认机制。这种设计既保证了数据安全性,又提供了良
回收站是现代应用中必不可少的功能,它为用户提供了一个安全网,防止误删除重要数据。在记事本应用中,回收站功能尤为重要,因为笔记往往包含用户的重要信息。本文将详细介绍如何实现一个功能完整的回收站页面,包括恢复笔记、永久删除和清空回收站等功能。
回收站的设计理念
回收站的核心思想是软删除。当用户删除一个笔记时,我们不是真正从数据库中删除它,而是给它打上一个"已删除"的标记。这样做有几个好处:首先,用户可以轻松恢复误删的笔记;其次,我们可以实现定时清理功能,比如自动删除30天前的笔记;最后,这种方式在技术实现上也更简单,不需要复杂的撤销机制。
在用户体验方面,回收站页面需要清晰地展示已删除的笔记,显示删除时间,并提供恢复和永久删除两个操作。我们还需要提供一个清空回收站的功能,让用户可以一次性删除所有已删除的笔记。这些功能的组合可以满足不同用户的需求。
页面的基础结构
让我们从回收站页面的基本代码开始:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../controllers/note_controller.dart';
import '../notes/widgets/empty_state.dart';
class TrashPage extends StatelessWidget {
const TrashPage({super.key});
回收站页面需要导入多个依赖包。Get用于状态管理和路由导航,flutter_screenutil用于屏幕适配,intl用于日期格式化。NoteController是核心的数据控制器,EmptyState用于显示空状态。
TrashPage是一个无状态Widget,因为它不需要管理自己的状态。所有的状态都由NoteController管理,页面只负责展示数据和响应用户操作。这种职责分离让代码更容易维护和测试。
Widget build(BuildContext context) {
final controller = Get.find<NoteController>();
return Scaffold(
appBar: AppBar(
title: const Text('回收站'),
actions: [
通过Get.find获取NoteController的实例,这是GetX依赖注入的标准用法。Scaffold提供了标准的Material Design布局结构,AppBar显示页面标题"回收站"。
AppBar的actions数组用于放置操作按钮。我们将在这里添加一个清空回收站的按钮,但这个按钮只在回收站不为空时显示。这种条件渲染可以避免用户在空回收站时看到无用的按钮。
Obx(() => controller.trashedNotes.isNotEmpty
? IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () => _confirmEmptyTrash(context, controller),
)
: const SizedBox()),
],
),
这里使用Obx包裹按钮,确保当回收站状态变化时按钮能够自动更新。如果trashedNotes不为空,就显示一个删除图标的按钮;如果为空,就显示一个空的SizedBox。
delete_forever图标表示永久删除,这个图标的语义很明确。点击按钮会调用_confirmEmptyTrash方法,显示一个确认对话框。这种二次确认机制可以防止用户误操作。
列表内容的展示
回收站的主体部分是已删除笔记的列表:
body: Obx(() {
final notes = controller.trashedNotes;
if (notes.isEmpty) {
return const EmptyState(
icon: Icons.delete_outline,
title: '回收站为空',
subtitle: '删除的笔记会在这里显示',
);
}
body使用Obx包裹,当笔记列表变化时会自动重建。我们从控制器获取trashedNotes,这是一个计算属性,会自动过滤出所有已删除的笔记。
如果回收站为空,就显示空状态组件。使用delete_outline图标,这是一个空心的删除图标,与回收站的语义相符。副标题告诉用户删除的笔记会在这里显示,这是一个很好的用户引导。
return ListView.builder(
padding: EdgeInsets.all(12.w),
itemCount: notes.length,
itemBuilder: (context, index) {
final note = notes[index];
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
如果回收站不为空,就使用ListView.builder显示列表。这是一个高效的列表组件,只会构建可见区域的item。padding设置为12个逻辑像素,让列表内容与屏幕边缘保持适当距离。
每个笔记使用Card包裹,提供统一的背景和阴影效果。Card之间的间距设置为8个逻辑像素,让列表看起来更有层次感。ListTile是Flutter提供的标准列表项组件,可以方便地展示标题、副标题和操作按钮。
title: Text(
note.title.isEmpty ? '无标题' : note.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'删除于 ${DateFormat('yyyy-MM-dd HH:mm').format(note.deletedAt!)}',
style: TextStyle(fontSize: 12.sp),
),
标题显示笔记的标题,如果标题为空就显示"无标题"。maxLines设置为1,overflow设置为ellipsis,这样长标题会被截断并显示省略号。这种处理方式可以保持列表的整洁。
副标题显示删除时间,使用DateFormat格式化日期。这个信息很重要,用户可以根据删除时间判断是否需要恢复笔记。note.deletedAt后面的感叹号表示我们确信这个值不为null,因为只有已删除的笔记才会出现在回收站中。
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.restore),
onPressed: () {
controller.restoreNote(note.id);
Get.snackbar('提示', '已恢复', snackPosition: SnackPosition.BOTTOM);
},
),
trailing部分显示操作按钮。我们使用Row来水平排列两个按钮,mainAxisSize设置为min让Row只占用必要的空间。
第一个按钮是恢复按钮,使用restore图标。点击后调用控制器的restoreNote方法恢复笔记,然后显示一个提示消息。Get.snackbar是GetX提供的便捷方法,可以快速显示一个底部提示。
IconButton(
icon: const Icon(Icons.delete_forever, color: Colors.red),
onPressed: () => _confirmPermanentDelete(context, controller, note),
),
],
),
),
);
},
);
}),
);
}
第二个按钮是永久删除按钮,使用delete_forever图标,颜色设置为红色以警示用户这是一个危险操作。点击后调用_confirmPermanentDelete方法,显示确认对话框。
这种双按钮的设计让用户可以快速选择恢复或永久删除。按钮的图标和颜色都经过精心设计,让用户能够直观地理解每个按钮的功能。
永久删除确认对话框
永久删除是一个不可逆的操作,我们需要通过对话框进行二次确认:
void _confirmPermanentDelete(BuildContext context, NoteController controller, note) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('永久删除'),
content: const Text('此操作不可恢复,确定要永久删除吗?'),
_confirmPermanentDelete方法使用showDialog显示一个AlertDialog。对话框的标题是"永久删除",内容明确告知用户这是一个不可恢复的操作。这种明确的警告可以让用户在操作前三思。
AlertDialog是Material Design中的标准对话框组件,它会自动处理很多细节,比如背景遮罩、动画效果、点击外部关闭等。我们只需要提供标题、内容和操作按钮即可。
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
controller.permanentDeleteNote(note.id);
Navigator.pop(context);
},
对话框提供两个按钮:取消和删除。取消按钮使用TextButton,这是一个扁平的按钮,适合次要操作。点击后直接关闭对话框,不执行任何操作。
删除按钮使用ElevatedButton,这是一个有背景色的按钮,更加醒目。背景色设置为红色,进一步强调这是一个危险操作。点击后调用控制器的permanentDeleteNote方法真正删除笔记,然后关闭对话框。
child: const Text('删除'),
),
],
),
);
}
按钮的文字简洁明了,"取消"和"删除"让用户清楚地知道每个按钮的作用。这种设计符合用户的心智模型,不会造成困惑。
对话框的整体设计遵循了Material Design的规范,包括布局、间距、字体大小等。这种标准化的设计让应用的界面保持一致性,也让用户感到熟悉和舒适。
清空回收站功能
清空回收站会删除所有已删除的笔记,这也需要确认:
void _confirmEmptyTrash(BuildContext context, NoteController controller) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('清空回收站'),
content: const Text('此操作将永久删除所有回收站中的笔记,不可恢复。'),
actions: [
_confirmEmptyTrash方法的结构与_confirmPermanentDelete类似,也是显示一个确认对话框。标题是"清空回收站",内容更加详细地说明了操作的后果:将永久删除所有笔记,不可恢复。
这种详细的说明很重要,因为清空回收站的影响范围更大。用户需要清楚地知道这个操作会影响多少笔记,以及操作的不可逆性。
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
controller.emptyTrash();
Navigator.pop(context);
按钮的设计与永久删除对话框相同,取消按钮和删除按钮。点击删除按钮后调用控制器的emptyTrash方法,这个方法会删除所有已删除的笔记。
emptyTrash方法的实现很简单,就是遍历所有笔记,删除那些isDeleted为true的笔记。这种批量操作在控制器中实现,页面只需要调用一个方法即可。
Get.snackbar('提示', '回收站已清空', snackPosition: SnackPosition.BOTTOM);
},
child: const Text('清空'),
),
],
),
);
}
}
操作完成后显示一个提示消息"回收站已清空",让用户知道操作已经成功执行。然后关闭对话框,返回回收站页面。此时回收站应该是空的,会自动显示空状态组件。
这种即时反馈很重要,它让用户知道操作已经完成,不会产生疑惑。GetX的snackbar方法让显示提示消息变得很简单,不需要复杂的代码。
控制器中的回收站相关方法
让我们看看NoteController中与回收站相关的实现:
class NoteController extends GetxController {
final RxList<Note> notes = <Note>[].obs;
final RxList<Category> categories = <Category>[].obs;
final RxList<Folder> folders = <Folder>[].obs;
final RxList<Tag> tags = <Tag>[].obs;
final RxList<NoteTemplate> templates = <NoteTemplate>[].obs;
final RxBool isDarkMode = false.obs;
final RxDouble fontSize = 16.0.obs;
控制器定义了多个响应式列表来管理不同类型的数据。notes列表包含所有的笔记,包括正常的和已删除的。通过isDeleted标记来区分它们。
这种设计的好处是所有笔记都在一个列表中,便于管理和持久化。我们不需要维护两个独立的列表,只需要通过过滤就可以得到不同状态的笔记。
final RxString sortBy = 'updatedAt'.obs;
final RxBool sortAscending = false.obs;
final _uuid = const Uuid();
void onInit() {
super.onInit();
loadData();
_initDefaultTemplates();
}
控制器还管理了一些全局设置,比如排序方式。onInit方法在控制器初始化时调用,加载持久化的数据并初始化默认模板。
这种初始化逻辑确保了应用启动时就有可用的数据。用户之前删除的笔记会被加载到回收站中,不会因为应用重启而丢失。
void deleteNote(String id) {
final index = notes.indexWhere((n) => n.id == id);
if (index != -1) {
notes[index] = notes[index].copyWith(
isDeleted: true,
deletedAt: DateTime.now(),
);
saveData();
}
}
deleteNote方法实现软删除。它找到对应的笔记,然后使用copyWith创建一个新的笔记对象,将isDeleted设置为true,并记录删除时间。
这种不可变的更新方式是函数式编程的思想。虽然在Dart中对象是可变的,但保持不可变性可以避免很多潜在的问题,比如意外修改、并发问题等。
void restoreNote(String id) {
final index = notes.indexWhere((n) => n.id == id);
if (index != -1) {
notes[index] = notes[index].copyWith(
isDeleted: false,
deletedAt: null,
);
saveData();
}
}
restoreNote方法用于恢复笔记。它将isDeleted设置回false,并清除deletedAt时间戳。这样笔记就会重新出现在正常的列表中,不再显示在回收站。
恢复操作的实现与删除类似,只是设置相反的值。这种对称的设计让代码更容易理解和维护。所有的修改操作都会调用saveData,确保数据及时持久化。
void permanentDeleteNote(String id) {
notes.removeWhere((n) => n.id == id);
saveData();
}
void emptyTrash() {
notes.removeWhere((n) => n.isDeleted);
saveData();
}
permanentDeleteNote方法真正删除笔记,使用removeWhere从列表中移除。这个操作是不可逆的,笔记会永久丢失。
emptyTrash方法删除所有已删除的笔记。它使用removeWhere过滤出所有isDeleted为true的笔记并删除。这是一个批量操作,可以一次性清空回收站。
计算属性的实现
控制器提供了一些计算属性来方便获取不同状态的笔记:
List<Note> get activeNotes => notes.where((n) => !n.isDeleted).toList();
List<Note> get trashedNotes => notes.where((n) => n.isDeleted).toList();
List<Note> get favoriteNotes => activeNotes.where((n) => n.isFavorite).toList();
List<Note> get pinnedNotes => activeNotes.where((n) => n.isPinned).toList();
activeNotes返回所有未删除的笔记,trashedNotes返回所有已删除的笔记。这两个getter是互补的,它们的并集就是所有笔记。
favoriteNotes和pinnedNotes都基于activeNotes,因为只有正常的笔记才能被收藏或置顶。已删除的笔记不应该出现在这些列表中。
这些计算属性让代码更加语义化。我们可以直接使用controller.trashedNotes,而不需要每次都写过滤逻辑。这种封装提高了代码的可读性和可维护性。
数据持久化的处理
回收站的数据需要持久化存储,让我们看看相关的实现:
Future<void> loadData() async {
final prefs = await SharedPreferences.getInstance();
final notesJson = prefs.getString('notes');
if (notesJson != null) {
final List<dynamic> decoded = jsonDecode(notesJson);
notes.value = decoded.map((e) => Note.fromJson(e)).toList();
}
loadData方法使用SharedPreferences来加载数据。笔记数据以JSON字符串的形式存储,加载时需要解码为对象列表。
已删除的笔记和正常的笔记存储在同一个列表中,通过isDeleted标记来区分。这种设计简化了数据管理,不需要维护多个存储键。
final categoriesJson = prefs.getString('categories');
if (categoriesJson != null) {
final List<dynamic> decoded = jsonDecode(categoriesJson);
categories.value = decoded.map((e) => Category.fromJson(e)).toList();
}
isDarkMode.value = prefs.getBool('isDarkMode') ?? false;
fontSize.value = prefs.getDouble('fontSize') ?? 16.0;
其他数据类型的加载方式相同。全局设置也需要加载,使用??运算符提供默认值。如果设置不存在,就使用默认值。
这种容错处理确保了应用在任何情况下都能正常运行。即使是首次启动,没有任何持久化数据,应用也能正常工作。
sortBy.value = prefs.getString('sortBy') ?? 'updatedAt';
}
Future<void> saveData() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('notes', jsonEncode(notes.map((e) => e.toJson()).toList()));
await prefs.setString('categories', jsonEncode(categories.map((e) => e.toJson()).toList()));
saveData方法将数据持久化到存储中。笔记列表转换为JSON字符串后保存。已删除的笔记也会被保存,这样应用重启后回收站中的笔记仍然存在。
每次数据变化时都会调用saveData,确保数据不会丢失。虽然这可能导致频繁的IO操作,但对于记事本这种应用,数据安全性比性能更重要。
使用示例
展示如何在应用中使用回收站功能。
// 在主页面中添加回收站入口
AppBar(
actions: [
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => Get.to(() => const TrashPage()),
),
],
),
// 在笔记控制器中添加删除方法
void deleteNote(String id) {
final note = notes.firstWhere((n) => n.id == id);
note.deletedAt = DateTime.now();
updateNotes();
Get.snackbar('提示', '已移至回收站');
}
// 在笔记控制器中添加恢复方法
void restoreNote(String id) {
final note = trashedNotes.firstWhere((n) => n.id == id);
note.deletedAt = null;
updateNotes();
Get.snackbar('提示', '已恢复');
}
// 在笔记控制器中添加永久删除方法
void permanentDeleteNote(String id) {
final note = trashedNotes.firstWhere((n) => n.id == id);
trashedNotes.remove(note);
updateNotes();
Get.snackbar('提示', '已永久删除');
}
使用示例展示了回收站的核心使用流程。在AppBar添加回收站入口按钮,使用Get.to导航。deleteNote方法设置deletedAt并更新,restoreNote方法清除deletedAt,permanentDeleteNote方法永久删除笔记。这些操作配合Get.snackbar提供用户反馈。
## 总结
回收站功能是记事本应用的重要组成部分,它为用户提供了一个安全网,防止误删除重要数据。通过软删除的设计,我们实现了一个功能完整且用户友好的回收站页面。
在实现过程中,我们使用了Flutter的组件化思想,将UI和业务逻辑分离。通过GetX进行状态管理,简化了代码的复杂度。通过SharedPreferences实现数据持久化,确保数据不会丢失。
回收站功能的设计需要平衡功能性和安全性。我们提供了恢复和永久删除两个操作,并通过确认对话框防止误操作。这种设计既满足了用户的需求,又保证了数据的安全。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)