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

鸿蒙Flutter实战:置顶功能的数据库与UI实现

前言

备忘录列表的第 0 条和第 1 条拥有最高的视觉优先级——用户打开应用第一眼看到的就是它们。如果用户有一条"本周待办汇总"的备忘录,每次都滚动到底部去找,体验是很糟糕的。

置顶功能正是解决这个问题的——把某条备忘录钉在列表最上方,无论列表怎么排序,它始终排在第一。微信聊天、邮件客户端、备忘录应用都有这个功能。

本文拆解鸿蒙 Flutter 备忘录中置顶功能的完整实现:从模型层的布尔字段,到数据库的排序逻辑,到 UI 的视觉区分和交互触发。

项目仓库:todo_flutter_harmony

模型层:isPinned 字段

classMemo{finalint?id;finalStringtitle;finalStringcontent;finalint?categoryId;finalbool isPinned;// ← 核心字段finalDateTimecreatedAt;finalDateTime?updatedAt;constMemo({this.id,requiredthis.title,this.content='',this.categoryId,this.isPinned=false,requiredthis.createdAt,this.updatedAt,});MemocopyWith({int?id,String?title,String?content,int?categoryId,bool?isPinned,DateTime?createdAt,DateTime?updatedAt,}){returnMemo(id:id??this.id,title:title??this.title,content:content??this.content,categoryId:categoryId??this.categoryId,isPinned:isPinned??this.isPinned,createdAt:createdAt??this.createdAt,updatedAt:updatedAt??this.updatedAt,);}Map<String,dynamic>toMap()=>{'id':id,'title':title,'content':content,'categoryId':categoryId,'isPinned':isPinned?1:0,// JSON 中存 0/1'createdAt':createdAt.millisecondsSinceEpoch,'updatedAt':updatedAt?.millisecondsSinceEpoch,};factoryMemo.fromMap(Map<String,dynamic>map)=>Memo(id:map['id'],title:map['title']??'',content:map['content']??'',categoryId:map['categoryId'],isPinned:(map['isPinned']??0)==1,createdAt:DateTime.fromMillisecondsSinceEpoch(map['createdAt']),updatedAt:map['updatedAt']!=null?DateTime.fromMillisecondsSinceEpoch(map['updatedAt']):null,);}

Provider 中的排序逻辑

排序规则很简单:先按isPinned降序(true 在前),再按createdAt降序(新的在前)。

classMemoProviderextendsChangeNotifier{List<Memo>_allMemos=[];List<Memo>getfilteredMemos{varresult=List<Memo>.from(_allMemos);// 分类过滤if(_categoryFilter!=null){result=result.where((m)=>m.categoryId==_categoryFilter).toList();}// 搜索过滤if(_searchQuery.isNotEmpty){result=result.where((m)=>m.title.toLowerCase().contains(_searchQuery.toLowerCase())||m.content.toLowerCase().contains(_searchQuery.toLowerCase())).toList();}// 排序:置顶优先 + 时间倒序result.sort((a,b){if(a.isPinned!=b.isPinned){returna.isPinned?-1:1;// true < false → true 排前面}returnb.createdAt.compareTo(a.createdAt);// 新的排前面});returnresult;}Future<void>togglePin(int id)async{finalmemo=_allMemos.firstWhere((m)=>m.id==id);finalupdated=memo.copyWith(isPinned:!memo.isPinned,updatedAt:DateTime.now(),);awaitDatabaseHelper.instance.updateMemo(updated);awaitloadMemos();}Future<void>loadMemos()async{_allMemos=awaitDatabaseHelper.instance.getAllMemos();notifyListeners();}}

关键细节:togglePin调用copyWith创建一个新对象(不可变模式),然后通过DatabaseHelper持久化,最后重新加载数据。这种方式保证数据一致性——UI 总是反映存储层的真实状态。

UI 中的置顶视觉区分

置顶的备忘录需要在视觉上与普通备忘录有所区别,但又不应该过于突兀:

classMemoCardextendsStatelessWidget{finalMemomemo;constMemoCard({super.key,requiredthis.memo});@overrideWidgetbuild(BuildContextcontext){returnAnimatedContainer(duration:constDuration(milliseconds:300),decoration:BoxDecoration(borderRadius:BorderRadius.circular(12),border:memo.isPinned?Border.all(color:constColor(0xFF4DB6AC).withOpacity(0.4),width:1):null,boxShadow:memo.isPinned?[BoxShadow(color:constColor(0xFF4DB6AC).withOpacity(0.08),blurRadius:8,offset:constOffset(0,2),),]:null,),child:Card(elevation:memo.isPinned?2:1,shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(12)),child:Padding(padding:constEdgeInsets.all(14),child:Row(crossAxisAlignment:CrossAxisAlignment.start,children:[// 置顶图钉图标if(memo.isPinned)Padding(padding:constEdgeInsets.only(right:8,top:2),child:Icon(Icons.push_pin,size:16,color:constColor(0xFF4DB6AC).withOpacity(0.7),),),// 内容区Expanded(child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:[Text(memo.title,style:constTextStyle(fontSize:16,fontWeight:FontWeight.w600,),),if(memo.content.isNotEmpty)...[constSizedBox(height:4),Text(memo.content,maxLines:2,overflow:TextOverflow.ellipsis,style:TextStyle(fontSize:14,color:Colors.grey.shade600,),),],],),),],),),),);}}

视觉设计要点:

  1. 图钉图标:仅置顶项显示,淡化颜色(70% 透明度),避免抢眼
  2. 边框:40% 透明度的主题色边框,暗示这是个"特殊"卡片
  3. 阴影:8% 透明度的主题色光晕,微微提亮
  4. elevation:从 1 升到 2,轻微的抬起感

触发置顶/取消置顶的交互

在滑动操作组件中置顶按钮:

SlideActionTile(leftActions:[SlideAction(label:memo.isPinned?'取消置顶':'置顶',icon:memo.isPinned?Icons.push_pin_outlined:Icons.push_pin,color:Colors.orange,onTap:()=>context.read<MemoProvider>().togglePin(memo.id!),),],// ...)

点击后,Provider 的togglePin切换isPinned状态 →loadMemos()重新加载并排序 →notifyListeners()重建列表。置顶的卡片瞬间移动到列表最上方,视觉上同时展示图钉图标、边框和阴影效果。

置顶数量的限制?

要不要限制置顶数量?有些应用限制最多 3 条置顶,防止置顶滥用。是否加这个限制取决于产品需求:

Future<void>togglePin(int id)async{finalmemo=_allMemos.firstWhere((m)=>m.id==id);// 如果要置顶,检查当前置顶数量if(!memo.isPinned){finalpinnedCount=_allMemos.where((m)=>m.isPinned).length;if(pinnedCount>=5){// 超出了,可以弹窗提醒或直接拒绝return;}}// 正常切换...}

鸿蒙 Flutter 备忘录应用目前没有加这个限制(用户数据量本就不大),但如果用户量增长,这是一个值得考虑的防御性设计。

DatabaseHelper 中的更新操作

classDatabaseHelper{Future<void>updateMemo(Memomemo)async{finalindex=_cache['memos']!.indexWhere((m)=>m['id']==memo.id);if(index!=-1){_cache['memos']![index]=memo.toMap();await_persistToFile();}}Future<void>_persistToFile()async{finaldir=awaitStoragePath.getAppDir();finalfile=File('$dir/.memo_app/data.json');awaitfile.writeAsString(jsonEncode(_cache));}}

由于使用的是纯 JSON 文件存储,更新操作就是:找到缓存中的对应项 → 替换 → 全量写入文件。对于个人备忘录这种数据量(通常几十到几百条),这个性能开销完全可以接受。

鸿蒙兼容性

置顶功能完全是数据层的逻辑——一个布尔字段的切换和排序规则的变化。不涉及任何平台 API,在 Android、iOS、鸿蒙 OHOS 上行为一致。

总结

置顶功能的实现可以分解为三层:

  1. 数据层isPinned: bool,JSON 中存 0/1
  2. 逻辑层:排序规则isPinned DESC, createdAt DESC
  3. UI 层:图钉图标 + 边框 + 阴影三重视觉区分,滑动操作触发togglePin

整个功能的核心代码不超过 20 行,但对用户体验的提升是显著的。

完整项目代码见:todo_flutter_harmony

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

相关文章:

  • 用Python和cryptography库模拟不经意传输(OT):一个隐私计算小实验
  • 毕业设计别再愁了!一个校园失物招领系统帮你搞定选题、设计与答辩
  • 微信WeChat-YATT框架:RLHF分布式训练优化实践
  • 脑机接口隐私风险解析:从数据安全到神经伦理的终极挑战
  • 2026年5月保定烽达模具机械厂:专注混凝土预制模具加工制造厂家 - 海棠依旧大
  • 保姆级教程:用CarSim 2020和Simulink手把手搭建平行泊车仿真(附MPC控制器模型)
  • 用Haskell依赖类型为TensorFlow占位符提供编译时安全保障
  • 鸿蒙Flutter实战:分类管理页BottomSheet CRUD
  • 基于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控制全流程实践