鸿蒙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;}}}三层降级策略:
- 优先尝试 MethodChannel(鸿蒙)
- 降级到
path_provider(标准平台) - 最后降级到
Directory.current.path(桌面/测试)
数据量评估
每行 JSON 按 300 字符(中文 + 元数据),1000 条数据约 300KB。全量读写的性能:
- 读取 300KB JSON + 解析:< 10ms(Dart 的 JSON 解析器是 C++ 实现的)
- 写入 300KB JSON:< 20ms(SSD)
在备忘录这种场景下,用户完全感知不到延迟。
迁移到 SQLite 的考虑
如果未来 App 用户增长、数据量达到万级,JSON 文件方案的性能会下降。此时需要一个迁移策略:
- 读取
data.json中的所有数据 - 批量写入 SQLite
- 修改
DatabaseHelper的实现(接口保持不变) - 删除或保留
data.json作为备份
因为所有数据访问都经过DatabaseHelper单例,替换底层存储方式不需要修改任何 Provider 或 UI 代码——这是分层架构的最大收益。
鸿蒙兼容性
dart:io的File、Directory类在鸿蒙 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
