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

📅 2026/7/13 5:41:08 👁️ 阅读次数 📝 编程学习
鸿蒙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