当前位置: 首页 > news >正文

鸿蒙Flutter实战:分类管理页BottomSheet CRUD

前言

备忘录的分类是动态的——用户需要能跟自己的需求变化新增、重命名、删除分类。“工作"和"个人"是初始分类,但随着时间推移,可能需要增加"健身”、“读书笔记”、"旅行计划"等分类。

鸿蒙 Flutter 备忘录提供了一个完整的分类管理页面,支持新增(BottomSheet 输入)、编辑(重命名)和删除(确认对话框)。本文拆解分类管理的完整 CRUD 实现,重点放在 BottomSheet 交互和删除时的数据迁移逻辑。

项目仓库:todo_flutter_harmony

分类管理页整体布局

classCategoryPageextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:constText('分类管理'),),body:Consumer<CategoryProvider>(builder:(context,provider,_){finalcategories=provider.categories;if(categories.isEmpty){returnCenter(child:Column(mainAxisSize:MainAxisSize.min,children:[Icon(Icons.folder_outlined,size:64,color:Colors.grey.shade300),constSizedBox(height:12),Text('暂无分类',style:TextStyle(color:Colors.grey.shade500)),constSizedBox(height:16),FilledButton.icon(onPressed:()=>_showAddCategorySheet(context,provider),icon:constIcon(Icons.add),label:constText('新建分类'),),],),);}returnListView.builder(itemCount:categories.length,itemBuilder:(context,index){return_buildCategoryItem(context,categories[index],provider);},);},),floatingActionButton:FloatingActionButton(onPressed:()=>_showAddCategorySheet(context,context.read<CategoryProvider>()),child:constIcon(Icons.add),),);}

新增分类:BottomSheet

新增分类不使用全屏页面导航,而是用showModalBottomSheet——这是一个从底部弹出的半屏面板,交互更轻量:

void_showAddCategorySheet(BuildContextcontext,CategoryProviderprovider,{MemoCategory?existingCategory}){finalnameController=TextEditingController(text:existingCategory?.name??'');finalemojiController=TextEditingController(text:existingCategory?.icon??'📋');finalisEditing=existingCategory!=null;showModalBottomSheet(context:context,isScrollControlled:true,// 键盘弹出时 BottomSheet 跟着上移shape:constRoundedRectangleBorder(borderRadius:BorderRadius.vertical(top:Radius.circular(20)),),builder:(ctx){returnPadding(padding:EdgeInsets.only(left:20,right:20,top:20,bottom:MediaQuery.of(ctx).viewInsets.bottom+20,// 为键盘留空间),child:Column(mainAxisSize:MainAxisSize.min,crossAxisAlignment:CrossAxisAlignment.stretch,children:[// 拖拽指示条Center(child:Container(width:40,height:4,decoration:BoxDecoration(color:Colors.grey.shade300,borderRadius:BorderRadius.circular(2),),),),constSizedBox(height:20),Text(isEditing?'编辑分类':'新建分类',style:constTextStyle(fontSize:18,fontWeight:FontWeight.bold),),constSizedBox(height:20),// Emoji 输入TextField(controller:emojiController,maxLength:2,decoration:constInputDecoration(labelText:'图标 (Emoji)',border:OutlineInputBorder(),counterText:'',),),constSizedBox(height:16),// 分类名称输入TextField(controller:nameController,autofocus:true,decoration:InputDecoration(labelText:'分类名称',border:constOutlineInputBorder(),errorText:_validateName(nameController.text),),),constSizedBox(height:20),// 确认按钮FilledButton(onPressed:(){finalname=nameController.text.trim();if(name.isEmpty)return;if(isEditing){provider.updateCategory(existingCategory!.copyWith(name:name,icon:emojiController.text),);}else{provider.addCategory(MemoCategory(name:name,icon:emojiController.text.isNotEmpty?emojiController.text:'📋',sortOrder:provider.categories.length,));}Navigator.pop(ctx);},child:Text(isEditing?'保存':'创建'),),constSizedBox(height:8),],),);},);}

关键细节:

  1. isScrollControlled: true:让 BottomSheet 在键盘弹出时自动上移,输入框不会被键盘遮挡
  2. MediaQuery.of(ctx).viewInsets.bottom:底部 padding 动态跟随键盘高度
  3. 拖拽指示条:顶部一个 40×4 的灰色小横条,暗示 BottomSheet 可以下拉关闭
  4. autofocus: true:打开 BottomSheet 后键盘自动弹出,聚焦到名称输入框
  5. 同一组件处理新增和编辑:通过isEditing参数区分

编辑分类

编辑复用同一个 BottomSheet,传入existingCategory参数即可:

Widget_buildCategoryItem(BuildContextcontext,MemoCategorycategory,CategoryProviderprovider){returnListTile(leading:Text(category.icon,style:constTextStyle(fontSize:24)),title:Text(category.name),trailing:PopupMenuButton<String>(onSelected:(value){switch(value){case'edit':_showAddCategorySheet(context,provider,existingCategory:category);break;case'delete':_confirmDeleteCategory(context,provider,category);break;}},itemBuilder:(ctx)=>[constPopupMenuItem(value:'edit',child:Text('编辑')),constPopupMenuItem(value:'delete',child:Text('删除')),],),);}

删除分类 + 数据迁移

删除分类时有一个关键问题:该分类下的备忘录怎么办?粗暴的做法是直接删除关联备忘录,但这对用户来说是数据丢失。更好的做法是:将关联的备忘录迁移到"未分类"(即categoryId = null)。

void_confirmDeleteCategory(BuildContextcontext,CategoryProviderprovider,MemoCategorycategory){showDialog(context:context,builder:(ctx)=>AlertDialog(title:constText('删除分类'),content:Text('确定要删除「${category.name}」分类吗?\n\n该分类下的备忘录将被移至"未分类"。',),actions:[TextButton(onPressed:()=>Navigator.pop(ctx),child:constText('取消'),),TextButton(onPressed:()async{awaitprovider.deleteCategory(category.id!);if(ctx.mounted){Navigator.pop(ctx);ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text('已删除分类「${category.name}」')),);}},style:TextButton.styleFrom(foregroundColor:Colors.red),child:constText('删除'),),],),);}

Provider 中的删除逻辑负责数据迁移:

Future<void>deleteCategory(int id)async{// 1. 将该分类下的所有备忘录设为"未分类"finaldb=DatabaseHelper.instance;finalmemos=awaitdb.getAllMemos();for(finalmemoinmemos){if(memo.categoryId==id){awaitdb.updateMemo(memo.copyWith(categoryId:null));}}// 2. 删除分类本身awaitdb.deleteCategory(id);// 3. 如果当前筛选器选中该分类,回退到"全部"if(_selectedCategoryId==id){_selectedCategoryId=null;}// 4. 重新加载awaitloadCategories();// 注意:需要通知 MemoProvider 也重新加载}

重名检测

新增分类时应检测同名:

String?_validateName(Stringname,CategoryProviderprovider,{MemoCategory?exclude}){if(name.trim().isEmpty)return'名称不能为空';finalexists=provider.categories.any((c)=>c.name.trim().toLowerCase()==name.trim().toLowerCase()&&c.id!=exclude?.id// 编辑时排除自身);if(exists)return'该分类名称已存在';returnnull;}

CategoryProvider 完整接口

classCategoryProviderextendsChangeNotifier{List<MemoCategory>_categories=[];List<MemoCategory>getcategories=>List.unmodifiable(_categories);Future<void>loadCategories()async{_categories=awaitDatabaseHelper.instance.getAllCategories();_categories.sort((a,b)=>a.sortOrder.compareTo(b.sortOrder));notifyListeners();}Future<void>addCategory(MemoCategorycategory)async{awaitDatabaseHelper.instance.insertCategory(category);awaitloadCategories();}Future<void>updateCategory(MemoCategorycategory)async{awaitDatabaseHelper.instance.updateCategory(category);awaitloadCategories();}Future<void>deleteCategory(int id)async{// 迁移关联备忘录到"未分类"finaldb=DatabaseHelper.instance;finalmemos=awaitdb.getAllMemos();for(finalmemoinmemos){if(memo.categoryId==id){awaitdb.updateMemo(memo.copyWith(categoryId:null));}}awaitdb.deleteCategory(id);awaitloadCategories();}}

鸿蒙兼容性

  • showModalBottomSheet:Material 组件,Flutter 框架层实现
  • MediaQuery.viewInsets:Flutter 框架从引擎获取键盘高度信息——这在鸿蒙上依赖flutter_ohos引擎正确报告键盘状态
  • 数据迁移逻辑:纯 Dart 代码,与平台无关

如果鸿蒙引擎在键盘高度报告上有不准确的情况,viewInsets.bottom可能不会是期望的值。这可以通过在 OHOS 端 EntryAbility 中做额外处理来修正。

总结

分类管理的 CRUD 实现关键点:

  1. 新增/编辑showModalBottomSheet+isScrollControlled,轻量半屏交互
  2. 删除:确认对话框 + 关联数据迁移到"未分类",防止数据丢失
  3. 重名检测:大小写不敏感的字符串比较
  4. BottomSheet 键盘适配viewInsets.bottom动态调整底部间距

完整项目代码见:todo_flutter_harmony

http://www.jsqmd.com/news/907957/

相关文章:

  • 基于YOLOv5与ESP32的智能垃圾分类系统:从AI视觉到硬件控制的完整实践
  • 终极热键侦探:3分钟快速定位Windows快捷键占用程序
  • 别再为BIM模型导入GIS发愁了!手把手教你用SuperMap插件搞定Revit/RVT文件
  • AI工具实战指南:消除工作损耗,重塑专业工作流
  • 2026年化粪池模具、检查井模具、流水槽模具、风电基础模板、水泥围墙模具厂家综合评测:用料、工艺、耐用度多维度行业分析 - 海棠依旧大
  • PyTorch如何重塑工程师思维:从动态图到模块化设计的工程实践
  • 告别XDMA限制:用开源Riffa框架在Linux下轻松搭建多通道PCIe DMA系统(Kintex-7实测)
  • Gemini多轮对话转化率提升全链路拆解(含用户意图熵值建模+动态响应阈值算法)
  • Spring Boot 3实战:5分钟用@HttpExchange搞定声明式HTTP客户端,告别OpenFeign
  • AI重塑客户关系:从智能客服到个性化体验的七大核心优势
  • AI时代文案人价值重构:从文字工作者到策略沟通者
  • 面试不再慌!Java面试常见问题及解答
  • 第12篇|记忆点点击:从 Marker 聚焦到照片详情面板
  • 从‘module ‘torch‘ has no attribute‘ 到成功运行GCN:一次完整的PyG环境排错实录
  • 别急着买机器人!用FANUC ROBOGUIDE的Handling Pro模块,零成本搞定涂胶方案验证
  • 保姆级教程:手动搞定Visual C++运行库,彻底解决Wireshark安装失败
  • 从MATLAB到FPGA板卡:手把手教你用COE文件为Xilinx FIR滤波器生成并加载系数
  • Python函数:位置参数与关键字参数的使用
  • Unity游戏开发:如何给Luban导表插件加上懒加载,告别启动卡顿(附完整模板修改教程)
  • 别再只盯着file://了!Gopher协议在SSRF中的高级利用与自动化Payload生成
  • 鸿蒙Flutter实战:放弃sqflite选纯Dart JSON文件存储
  • 从零构建自动驾驶小车:树莓派+CNN+PID控制全流程实践
  • 大语言模型内部机制探查:Patchscopes框架与可解释性实践
  • Java面试技巧全攻略:从简历到现场问答
  • PyTorch训练时遇到‘indices should be on the same device’报错?别慌,5分钟教你定位并修复这个GPU/CPU设备不匹配问题
  • 保姆级教程:用USB Burning Tool给UNT413A盒子刷S905L3A纯净固件(附固件下载)
  • 工业视觉实战:用Halcon measure_pairs精准测量零件卡槽宽度(避坑IntraDistance与InterDistance)
  • Java与Spring框架整合:快速构建企业级应用
  • 告别高延迟!在Unity中低延时接入海康威视摄像头的两种实战方案(UMP vs SDK)
  • Keil C51函数地址优化与模块级定位技术详解