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

鸿蒙Flutter实战:放弃sqflite选纯Dart JSON文件存储

前言

Flutter 应用中做本地持久化,第一反应通常是sqflite。它成熟、稳定、API 友好——但它是原生插件,需要 Android 的 SQLite 库,需要 iOS 的 FMDB。

问题来了:鸿蒙 OHOS 没有 SQLite 原生支持。或者说,至少没有标准化的、与sqflite插件兼容的 SQLite 绑定。如果要让鸿蒙也能跑sqflite,意味着自己写一整套 OHOS 端的 FFI 绑定——这对于一个个人备忘录应用来说,投入产出比失衡。

于是做了一个关键架构决策:放弃 sqflite,用纯 Dart 的dart:io+dart:convert实现 JSON 文件存储

本文详述这个决策背后的工程权衡和完整实现。

项目仓库:todo_flutter_harmony

决策分析:JSON 文件 vs SQLite

维度sqflite纯 Dart JSON 文件
鸿蒙兼容性❌ 需要原生 FFI 绑定dart:io天然支持
查询能力✅ SQL 完整查询⚠️ Dart 内存过滤
写入性能✅ 增量写入⚠️ 全量重写 JSON
数据一致性✅ 事务支持⚠️ 手动保证
并发安全✅ 连接池⚠️ 单线程无竞态
代码量中等(需要 migration)少(100 行左右)
适合数据量万级以上千级以下

对于个人备忘录应用(预计数据量 < 1000 条),JSON 文件的劣势不明显,而鸿蒙兼容性的优势是决定性的。

架构设计:单例 + 内存缓存

核心思路:所有数据在内存中维护一份Map<String, dynamic>缓存,CRUD 操作修改缓存后全量写回 JSON 文件。

classDatabaseHelper{// 单例模式staticfinalDatabaseHelperinstance=DatabaseHelper._();DatabaseHelper._();// 内存缓存Map<String,dynamic>_cache={};// 自增 ID 计数器int _nextMemoId=1;int _nextTodoId=1;int _nextDiaryId=1;int _nextCategoryId=1;// 是否已初始化bool _initialized=false;

初始化:加载 JSON 文件到内存

Future<void>init()async{if(_initialized)return;finaldir=awaitStoragePath.getAppDir();finaldataDir=Directory('$dir/.memo_app');finaldataFile=File('$dir/.memo_app/data.json');// 确保目录存在if(!awaitdataDir.exists()){awaitdataDir.create(recursive:true);}// 读取已有数据if(awaitdataFile.exists()){finalcontent=awaitdataFile.readAsString();if(content.isNotEmpty){_cache=jsonDecode(content)asMap<String,dynamic>;}}// 初始化缓存结构_cache.putIfAbsent('memos',()=>[]);_cache.putIfAbsent('todos',()=>[]);_cache.putIfAbsent('diaries',()=>[]);_cache.putIfAbsent('categories',()=>[]);// 初始化自增 ID_nextMemoId=_getMaxId(_cache['memos'])+1;_nextTodoId=_getMaxId(_cache['todos'])+1;_nextDiaryId=_getMaxId(_cache['diaries'])+1;_nextCategoryId=_getMaxId(_cache['categories'])+1;_initialized=true;}int_getMaxId(Listlist){if(list.isEmpty)return0;returnlist.fold<int>(0,(max,item){finalid=(itemasMap)['id']asint???0;returnid>max?id:max;});}

CRUD 操作:以 Memo 为例

// ========== Memo CRUD ==========Future<List<Memo>>getAllMemos()async{awaitinit();finallist=_cache['memos']asList;returnlist.map((json)=>Memo.fromMap(jsonasMap<String,dynamic>)).toList();}Future<void>insertMemo(Memomemo)async{awaitinit();finalmemos=_cache['memos']asList;finalnewMemo=memo.toMap()..['id']=_nextMemoId++;memos.add(newMemo);await_persistToFile();}Future<void>updateMemo(Memomemo)async{awaitinit();finalmemos=_cache['memos']asList;finalindex=memos.indexWhere((m)=>m['id']==memo.id);if(index!=-1){memos[index]=memo.toMap();await_persistToFile();}}Future<void>deleteMemo(int id)async{awaitinit();finalmemos=_cache['memos']asList;memos.removeWhere((m)=>m['id']==id);await_persistToFile();}

全量持久化

Future<void>_persistToFile()async{finaldir=awaitStoragePath.getAppDir();finalfile=File('$dir/.memo_app/data.json');finaljsonString=constJsonEncoder.withIndent(' ').convert(_cache);awaitfile.writeAsString(jsonString);}

JsonEncoder.withIndent(' ')生成格式化 JSON,方便开发者调试时直接打开data.json查看数据。生产环境可以用不带缩进的版本减小文件体积。

文件锁和并发安全

由于 Flutter/Dart 是在单个 isolate 中运行(不涉及多线程并发),不存在两个操作同时写入文件的问题。所有异步操作在 event loop 上排队执行,天然的串行保证。

但如果应用未来引入Isolate做后台处理,需要考虑文件锁:

Future<void>_persistToFile()async{finaldir=awaitStoragePath.getAppDir();finalfile=File('$dir/.memo_app/data.json');// 先写入临时文件,再原子替换finaltempFile=File('$dir/.memo_app/data.tmp.json');finaljsonString=jsonEncode(_cache);awaittempFile.writeAsString(jsonString);awaittempFile.rename(file.path);// 原子操作}

"写临时文件再 rename"是一种常见的原子写入策略:如果在写入过程中应用崩溃,损坏的是临时文件,正式文件保持完整。

StoragePath:获取应用目录

classStoragePath{staticconst_channel=MethodChannel('com.memo.app/storage');staticFuture<String>getAppDir()async{try{// 鸿蒙 OHOS:通过 MethodChannel 获取 filesDirfinaldir=await_channel.invokeMethod<String>('getFilesDir');if(dir!=null&&dir.isNotEmpty)returndir;}catch(e){// MethodChannel 不可用时静默降级}// Android / iOS / Desktop:使用 path_providertry{finaldir=awaitgetApplicationDocumentsDirectory();returndir.path;}catch(e){// 最后的降级:当前目录returnDirectory.current.path;}}}

三层降级策略:

  1. 优先尝试 MethodChannel(鸿蒙)
  2. 降级到path_provider(标准平台)
  3. 最后降级到Directory.current.path(桌面/测试)

数据量评估

每行 JSON 按 300 字符(中文 + 元数据),1000 条数据约 300KB。全量读写的性能:

  • 读取 300KB JSON + 解析:< 10ms(Dart 的 JSON 解析器是 C++ 实现的)
  • 写入 300KB JSON:< 20ms(SSD)

在备忘录这种场景下,用户完全感知不到延迟。

迁移到 SQLite 的考虑

如果未来 App 用户增长、数据量达到万级,JSON 文件方案的性能会下降。此时需要一个迁移策略:

  1. 读取data.json中的所有数据
  2. 批量写入 SQLite
  3. 修改DatabaseHelper的实现(接口保持不变)
  4. 删除或保留data.json作为备份

因为所有数据访问都经过DatabaseHelper单例,替换底层存储方式不需要修改任何 Provider 或 UI 代码——这是分层架构的最大收益。

鸿蒙兼容性

dart:ioFileDirectory类在鸿蒙 OHOS 上依赖 Flutter OHOS 引擎提供的文件系统绑定。@ohos/flutter_ohos引擎已经实现了这些绑定,因为它是运行 Flutter 最基本的要求。JSON 编码解码在 Dart VM 层完成,与平台无关。

总结

为了鸿蒙兼容性放弃 sqflite、选择纯 Dart JSON 文件存储,这是一个工程权衡的经典案例。核心判断依据是:个人备忘录的数据量在 JSON 文件方案的性能承受范围内,而鸿蒙兼容性是不可放弃的硬需求

100 行代码的DatabaseHelper实现了完整的 CRUD + 自增 ID + 原子写入,没有任何平台绑定——这就是 Flutter “write once, run anywhere” 理念的最佳实践。

完整项目代码见:todo_flutter_harmony

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

相关文章:

  • 从零构建自动驾驶小车:树莓派+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函数地址优化与模块级定位技术详解
  • 第13篇|景点 POI 叠加:附近推荐如何和照片记忆共存
  • Million-AID数据集长尾分布怎么办?手把手教你用PyTorch实现类别平衡采样
  • 基于Arduino的商用咖啡机自动化改造:从流量计感知到继电器控制
  • 病灶溯源:论波普尔证伪主义作为西方伪科学体系的逻辑毒根
  • 用STM32F103C8T6和PCA9685驱动板,我让12个SG90舵机‘听话’地走起来了(附完整代码)
  • 告别信号死角:手把手解读3GPP R17覆盖增强的三大核心黑科技(PUSCH/TBoMS/DMRS)
  • 别再死记硬背命令了!用华为eNSP模拟器,从零搭建一个高可用企业网(VRRP+MSTP+OSPF实战)
  • AI赋能万尺空间:从感知到决策的智能化转型实践
  • 用C++和Eigen手撸一个MINCO轨迹优化器:从论文复现到避坑实战
  • 避开SCARA机器人工作空间规划的坑:从DH建模到奇异点分析与MATLAB可视化
  • Heroku上快速部署PostGIS:从零构建地理空间数据库实战
  • 从Faster R-CNN到Oriented R-CNN:在DOTA数据集上实战旋转目标检测(附完整训练配置)
  • 用Matlab和Robotics Toolbox搞定SCARA机器人建模:从DH参数到工作空间可视化(附KUKA KR 6 R500 Z200实例代码)
  • 第14篇|LocationKit 取当前位置:成功、失败、精度不足都要可解释
  • 告别WebGL!用Unity Embedded Browser插件在PC端打造高性能混合UI(含本地HTML与JS双向通信详解)
  • 8051单片机I/O端口锁存器原理与工程实践
  • 搜索引擎集成AI口语教练:技术原理、应用场景与实战指南
  • 从钽电容烧毁到系统稳定:我的电源滤波电路“踩坑”与修复实录
  • 从模拟退火到量子退火:一个物理学家的奇思妙想是如何变成D-Wave机器的
  • 别再到处找镜像了!保姆级CentOS 7.6安装包下载与VMware虚拟机配置全流程
  • SAE J1939-71实战避坑指南:从‘F004’到‘SPN 190’,新手最容易误解的3个数据解析细节