当前位置: 首页 > 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.zskr.cn/news/1419267.html

相关文章:

  • 微信WeChat-YATT框架:RLHF分布式训练优化实践
  • 保姆级教程:用CarSim 2020和Simulink手把手搭建平行泊车仿真(附MPC控制器模型)
  • AI工具实战指南:消除工作损耗,重塑专业工作流
  • Gemini多轮对话转化率提升全链路拆解(含用户意图熵值建模+动态响应阈值算法)
  • 别再只盯着file://了!Gopher协议在SSRF中的高级利用与自动化Payload生成
  • 从零构建自动驾驶小车:树莓派+CNN+PID控制全流程实践
  • 鸿蒙Flutter实战:放弃sqflite选纯Dart JSON文件存储
  • 大语言模型内部机制探查:Patchscopes框架与可解释性实践
  • Java面试技巧全攻略:从简历到现场问答
  • PyTorch训练时遇到‘indices should be on the same device’报错?别慌,5分钟教你定位并修复这个GPU/CPU设备不匹配问题
  • Keil C51函数地址优化与模块级定位技术详解
  • AI赋能万尺空间:从感知到决策的智能化转型实践
  • 从Faster R-CNN到Oriented R-CNN:在DOTA数据集上实战旋转目标检测(附完整训练配置)
  • 8051单片机I/O端口锁存器原理与工程实践
  • 第15篇|定位权限体验:先讲清用途,再让用户授权
  • 提升生成式AI上下文置信度:从原理到工程实践
  • STM32F103上给LVGL加触摸,我用野火开发板踩过的坑都在这了
  • 自学程序员求职指南:从简历重构到面试通关的实战策略
  • 从理想传输线到真实PCB:ADS中微带双枝短截线匹配的完整实战与参数优化
  • C51开发中全局与静态变量初始化问题解析
  • HFSS实战:手把手教你用参数扫描和优化功能,搞定2.45GHz矩形贴片天线匹配
  • 别再手动写Watermark了!WPF文本框Placeholder的三种主流实现方案(附完整源码)
  • DS-5环境下Arm Linux C/C++项目创建与配置指南
  • 新手网工别懵圈!华为AC+瘦AP旁挂上线,保姆级配置命令逐行解析
  • Coral NPU:基于RISC-V的开放架构如何重塑边缘AI开发范式
  • WSL2虚拟磁盘迁移后,如何像原来一样丝滑使用?配置默认用户和优化路径的完整指南
  • 大语言模型在糖尿病管理中的应用:架构、场景与挑战
  • 保姆级教程:用Docker Buildx搞定ARM和x86镜像,一键推送到自建私有仓库
  • GazeProphet:无硬件依赖的VR注视点预测技术解析
  • 告别环境配置噩梦:用最新方法在Ubuntu 20.04快速部署PX4与ROS1开发栈