请添加图片描述

前言

单条删除用滑动操作就够了。但如果用户想一口气删除十几条过期的备忘录,一条一条滑得累死。批量删除需要一套"选择模式"——用户通过长按进入选择态,勾选多个条目,然后一键删除。

这和文件管理器、相册的多选逻辑是一样的:长按激活选择模式 → 点击勾选/取消 → 全选/反选 → 批量操作。本文拆解鸿蒙 Flutter 备忘录中这个功能的完整实现。

项目仓库:todo_flutter_harmony

状态设计

选择模式需要在 MemoProvider 中维护两组状态:

class MemoProvider extends ChangeNotifier {
  bool _isSelectionMode = false;
  final Set<int> _selectedIds = {};

  bool get isSelectionMode => _isSelectionMode;
  Set<int> get selectedIds => Set.unmodifiable(_selectedIds);
  int get selectedCount => _selectedIds.length;

  // 进入选择模式
  void enterSelectionMode(int initialId) {
    _isSelectionMode = true;
    _selectedIds.add(initialId);
    notifyListeners();
  }

  // 退出选择模式
  void exitSelectionMode() {
    _isSelectionMode = false;
    _selectedIds.clear();
    notifyListeners();
  }

  // 切换某个条目的选中状态
  void toggleSelection(int id) {
    if (_selectedIds.contains(id)) {
      _selectedIds.remove(id);
      if (_selectedIds.isEmpty) {
        _isSelectionMode = false;  // 全部取消后自动退出选择模式
      }
    } else {
      _selectedIds.add(id);
    }
    notifyListeners();
  }

  // 全选
  void selectAll() {
    _selectedIds.addAll(_allMemos.map((m) => m.id!));
    notifyListeners();
  }

  // 取消全选
  void deselectAll() {
    _selectedIds.clear();
    _isSelectionMode = false;
    notifyListeners();
  }

  // 批量删除
  Future<void> deleteSelected() async {
    for (final id in _selectedIds) {
      await DatabaseHelper.instance.deleteMemo(id);
    }
    _selectedIds.clear();
    _isSelectionMode = false;
    await loadMemos();
  }
}

核心设计要点:

  • _selectedIds 是一个 Set<int>,O(1) 的查找和删除效率
  • 所有选中项都被取消后自动退出选择模式
  • deleteSelected 是异步方法,逐条删除后重新加载数据

AppBar 的动态切换

选择模式下,AppBar 从"备忘录列表"变为"批量操作栏":

class MemoListPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: Consumer<MemoProvider>(
        builder: (context, provider, _) {
          if (provider.isSelectionMode) {
            return _buildSelectionAppBar(provider);
          }
          return _buildNormalAppBar(context);
        },
      ),
      body: _buildBody(),
    );
  }

选择模式 AppBar

AppBar _buildSelectionAppBar(MemoProvider provider) {
  return AppBar(
    leading: IconButton(
      icon: const Icon(Icons.close),
      onPressed: () => provider.exitSelectionMode(),
    ),
    title: Text('已选择 ${provider.selectedCount} 项'),
    actions: [
      // 全选/取消全选
      TextButton(
        onPressed: () {
          if (provider.selectedCount == provider.memos.length) {
            provider.deselectAll();
          } else {
            provider.selectAll();
          }
        },
        child: Text(
          provider.selectedCount == provider.memos.length ? '取消全选' : '全选',
          style: const TextStyle(color: Colors.white),
        ),
      ),
      // 删除按钮
      IconButton(
        icon: const Icon(Icons.delete_outline),
        onPressed: provider.selectedCount > 0
            ? () => _confirmBatchDelete(context, provider)
            : null,
      ),
    ],
    backgroundColor: const Color(0xFFE53935),  // 红色背景警告态
  );
}

红色背景的 AppBar 在视觉上给用户一个明确的信号——“你在操作模式中,小心”。

删除确认对话框

批量删除是不可逆操作,必须有确认环节:

void _confirmBatchDelete(BuildContext context, MemoProvider provider) {
  showDialog(
    context: context,
    builder: (ctx) => AlertDialog(
      title: const Text('确认删除'),
      content: Text('确定要删除选中的 ${provider.selectedCount} 条备忘录吗?此操作不可撤销。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(ctx),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            provider.deleteSelected();
            Navigator.pop(ctx);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('已删除 ${provider.selectedCount} 条备忘录')),
            );
          },
          style: TextButton.styleFrom(foregroundColor: Colors.red),
          child: const Text('删除'),
        ),
      ],
    ),
  );
}

列表项的多选 UI

选择模式下的列表项需要显示复选框:

Widget _buildMemoItem(Memo memo, MemoProvider provider) {
  final isSelected = provider.selectedIds.contains(memo.id);

  return GestureDetector(
    onLongPress: () {
      if (!provider.isSelectionMode) {
        provider.enterSelectionMode(memo.id!);
        HapticFeedback.mediumImpact();  // 触觉反馈
      }
    },
    onTap: () {
      if (provider.isSelectionMode) {
        provider.toggleSelection(memo.id!);
      } else {
        // 正常模式:打开编辑页
        Navigator.pushNamed(context, '/memo/edit', arguments: memo.id);
      }
    },
    child: AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      color: isSelected
          ? const Color(0xFF4DB6AC).withOpacity(0.08)
          : Colors.transparent,
      child: Row(
        children: [
          // 选择模式下显示复选框
          if (provider.isSelectionMode)
            Padding(
              padding: const EdgeInsets.only(left: 16),
              child: Icon(
                isSelected
                    ? Icons.check_circle
                    : Icons.radio_button_unchecked,
                color: isSelected
                    ? const Color(0xFF4DB6AC)
                    : Colors.grey.shade400,
              ),
            ),
          // 卡片内容
          Expanded(child: MemoCard(memo: memo)),
        ],
      ),
    ),
  );
}

关键交互细节:

  1. 长按进入选择模式onLongPress 触发 enterSelectionMode,同时播放 HapticFeedback.mediumImpact() 触觉反馈
  2. 点击行为分流:选择模式下点击 = 勾选/取消,正常模式下点击 = 打开编辑页
  3. 选中背景色AnimatedContainercolor 从透明过渡到 8% 透明度主题色
  4. 复选框 icon:用 check_circle / radio_button_unchecked 替代 Material Checkbox widget,风格更轻量

防止内存泄漏:批量删除中的 mounted 检查

批量删除是异步操作,删除过程中用户可能导航到其他页面:

Future<void> deleteSelected() async {
  final idsToDelete = Set<int>.from(_selectedIds);
  _selectedIds.clear();
  _isSelectionMode = false;
  notifyListeners();

  for (final id in idsToDelete) {
    await DatabaseHelper.instance.deleteMemo(id);
  }

  await loadMemos();
  // loadMemos 会触发 notifyListeners()
}

注意:此处先把 _selectedIds 拷贝一份再清空,UI 立即更新(移除红色 AppBar),然后在后台逐条删除。如果用户在删除过程中导航离开,widget 已被 dispose,loadMemos 中如果有 notifyListeners() 调用,listener 不会引发崩溃——Provider 已经处理了这种情况。

鸿蒙兼容性

批量删除的实现完全在 Dart 层:

  • Set<int> 状态管理:Dart 标准库
  • HapticFeedback:Flutter services 层,在鸿蒙上调用 OHOS 振动 API(需确认 flutter_ohos 引擎是否实现了振动能力)
  • showDialog / AlertDialog:Material 组件,纯 Flutter 实现

如果鸿蒙设备不支持 HapticFeedback,mediumImpact() 会静默失败(不会抛异常)。这是一个很好的防御性编程实践。

总结

多选批量删除模式的关键在于状态转换:

  1. 长按isSelectionMode = true,AppBar 切换为红色操作栏
  2. 点击toggleSelection(id)Set<int> 增删元素
  3. 点击删除 → 确认对话框 → 逐条删除 → 重新加载
  4. 取消/完成exitSelectionMode() 清空状态

总共约 100 行 Provider 代码,60 行 UI 代码,实现了一套符合移动端直觉的批量操作交互。

完整项目代码见:todo_flutter_harmony

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐