回收站是现代应用中必不可少的功能,它为用户提供了一个安全网,防止误删除重要数据。在记事本应用中,回收站功能尤为重要,因为笔记往往包含用户的重要信息。本文将详细介绍如何实现一个功能完整的回收站页面,包括恢复笔记、永久删除和清空回收站等功能。
请添加图片描述

回收站的设计理念

回收站的核心思想是软删除。当用户删除一个笔记时,我们不是真正从数据库中删除它,而是给它打上一个"已删除"的标记。这样做有几个好处:首先,用户可以轻松恢复误删的笔记;其次,我们可以实现定时清理功能,比如自动删除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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐