OpenHarmony上Flutter本地数据持久化全攻略:从SharedPreferences到SQLite与文件存储

发布时间:2026/9/14 12:59:20
OpenHarmony上Flutter本地数据持久化全攻略:从SharedPreferences到SQLite与文件存储 训练营到了第12天前面我们一直在折腾页面怎么搭、状态怎么管、接口怎么调今天终于要碰一个看起来不起眼、但几乎每款应用都绕不开的话题本地数据持久化。而且这堂课放在OpenHarmony的Flutter跨平台系列里比单纯讲Flutter存储又多了一层意思——同样的API在鸿蒙生态下跑起来底层路径、沙箱规则、插件适配都有各自的脾气。先说结论如果你正准备用Flutter做OpenHarmony应用开发或者已经在HarmonyOS Next上踩过数据存不下来、重启就丢、路径找不到之类的坑这篇内容基本就是你需要的“排雷手册”。我会把这几天训练营的教学思路、方案选型、代码实现和真实踩坑记录全部拆开讲从SharedPreferences到SQLite再到文件存储一条链路走完。1. 为什么在鸿蒙上做Flutter持久化先要把“存什么”想清楚1.1 训练营Day 12-13安排这个主题的底层逻辑很多跨平台开发者容易陷入一个误区上来就选一个存储插件然后像在Android、iOS上那样直接写代码。但本地持久化在所有客户端开发里天然就是那种“前期不做规划、后期疯狂返工”的模块。训练营之所以把本地数据持久化单独拎出来放在第12、13天是因为前面几天刚好把页面和状态管理串完了这时候学员手里通常已经有一个能跑通基本业务流程的App雏形。有了雏形才有真实的数据需要存储才知道哪些数据应该落盘、哪些数据放在内存里就够了。在OpenHarmony的应用开发里本地数据大概分四类用户配置类主题色、字体大小、登录状态、开关选项特点是轻量、零散、变化频率不高。结构化业务数据笔记列表、任务清单、收藏记录特点是字段固定、需要增删改查。缓存文件图片、日志、下载的Zip包特点是体积大、生命周期短、丢失了可以重新拉取。复杂对象序列化比如把整个页面状态快照保存下来这种需求相对少见通常会用JSON字符串存到键值库或文件里。这四类数据对应的存储方案完全不同不能一把梭。训练营的解法是先帮学员建立“分类”意识再对不同分类选择对应的Flutter生态插件和OpenHarmony底层能力。另外要说明的是OpenHarmony的分布式特性决定了它在存储层面上比普通移动端多了一个维度多设备协同。在手机、平板、电视之间流转时数据是否需要同步、以哪个设备为准、冲突怎么合并这些都会直接影响存储方案的选择。不过训练营的Day 12-13只聚焦“单设备本地持久化”分布式数据同步是后面几天的内容这里先不展开。1.2 鸿蒙侧存储能力与Flutter方案如何一一对应很多初学者以为Flutter在OpenHarmony上做存储就是插件包直接翻译一遍。实际情况是Flutter应用跑在OpenHarmony设备上时Dart层的API走的是Platform Channel最终落地到鸿蒙原生的存储服务上。所以从Flutter层看你用的是SharedPreferences、sqflite这类通用插件但在OpenHarmony上底层实现其实被映射到了鸿蒙的Preferences、关系型数据库等能力上。我把对应的关系整理成了表格Flutter侧常用方案底层数据形态OpenHarmony底层映射适合场景shared_preferences键值对Preferences系统偏好存储用户配置、开关项、轻量标记sqflite / driftSQLite数据库表鸿蒙关系型数据库RDB或SQLite FFI结构化业务数据增删改查频繁的数据path_provider 文件读写文件应用沙箱文件目录图片缓存、日志、大文件hive无模型二进制对象自定义二进制文件需要快速读写复杂对象的场景理解这层关系很重要。因为在实际开发中你经常会遇到“Flutter层API完全一样但运行结果表现不同”的情况。比如SharedPreferences在Android上默认是XML文件、在iOS上是NSUserDefaults在OpenHarmony上则是基于分布式数据服务的偏好存储。它们在读写速度、持久化时机、跨进程可见性上都有差异这些差异往往就是bug的来源。另一个常见的疑惑是为什么不能直接在OpenHarmony上调用鸿蒙原生的Preferences API技术上当然可以通过MethodChannel自己封装就行但这样做会让Dart层代码和平台强耦合完全放弃了Flutter跨平台的优势。训练营的原则是只要Flutter生态里已有插件能满足需求就优先用插件方案只有插件方案在鸿蒙上表现不可接受时才考虑自己封装。2. 三套主流持久化方案在OpenHarmony上的真实适配体验2.1 键值存储SharedPreferences是首选但别把它当数据库用如果只是保存登录token、主题色、启动次数这类简单配置SharedPreferences就是最省心的选择。在Flutter里它的用法大家应该已经很熟了import package:shared_preferences/shared_preferences.dart; // 写入 final prefs await SharedPreferences.getInstance(); await prefs.setString(user_token, xxx); await prefs.setBool(dark_mode, true); // 读取 final token prefs.getString(user_token); final darkMode prefs.getBool(dark_mode) ?? false;但在OpenHarmony上使用时有几个点要特别注意。首先getInstance()方法在Dart层是有缓存机制的——同一个Isolate内第一次调用后会缓存实例后续调用直接返回内存中的对象。这意味着你在一个Isolate里写入数据后另一个Isolate并不会自动感知。训练营里就有学员在创建了一个Isolate去处理网络请求然后在子Isolate里读取SharedPreferences结果发现数据是空的。这不是bug而是设计如此。解决办法就是在子Isolate里通过SendPort把主Isolate读到的值传过去或者干脆在子Isolate里重新调用getInstance()内部会重新读取底层数据。其次prefs.setString()这类写操作在Dart层是异步的但数据真正写入磁盘还需要一点时间。在Android上有时你写完后立刻杀掉进程数据可能来不及落盘在OpenHarmony上同样存在类似问题。如果要在App退出前确保数据写完官方提供的prefs.reload()和prefs.waitForPendingWrites()就能派上用场。我在训练营里给的建议是重要的数据比如用户登录状态在AppLifecycleState.detached或paused时调用一次waitForPendingWrites()避免系统回收进程导致数据丢失。另外SharedPreferences不适合存大量数据。它本质上是全量读入内存的数据量越大启动时反序列化耗时越长。如果你发现一个键值库里存了几百上千个key就该考虑把这些数据拆出去放到后面说的关系型数据库里。注意在OpenHarmony的Flutter适配工程中shared_preferences插件底层已经对接到了鸿蒙的Preferences能力所以在大多数场景下可以无感使用。但无论是用插件还是自己封装平台通道都不建议用它存超过50KB的单个value这是经验值不是硬限制。2.2 结构化数据sqflite在鸿蒙上的两条路当数据变成列表、需要条件查询、需要事务操作时SharedPreferences就不够看了。这时候正规军是数据库。在Flutter生态里最成熟的数据库方案是sqflite其次是以sqflite为基础的Drift。但是在OpenHarmony上sqflite的适配情况稍微有点特殊。默认的sqflite插件依赖的是Android/iOS的原生SQLite接口在鸿蒙上直接跑是行不通的。目前主流做法有两条第一条路是用sqflite_common_ffi。这个包的原理是在Dart层通过FFI直接调用SQLite的C语言接口完全绕开了平台原生通道。也就是说它根本不依赖Android或iOS的SQLite只要鸿蒙设备上能编译出SQLite的动态库就能跑起来。第二条路是自己通过MethodChannel封装鸿蒙原生的关系型数据库接口RDB。这条路更“鸿蒙原生化”性能理论上更好但需要自己写不少胶水代码而且只适用于OpenHarmony放弃了跨平台性。从训练营的角度我建议绝大多数人走第一条路。原因很简单代码复用度高团队里已有的Flutter数据库层代码可以直接迁移到鸿蒙上不需要重新研发。实测下来sqflite_common_ffi在模拟器上的表现相当稳定事务、索引、批量插入这些常规操作都能正常工作。使用sqflite_common_ffi的初始化方式如下import package:sqflite_common_ffi/sqflite_ffi.dart; void main() { // 在OpenHarmony等桌面/非移动平台需要先初始化FFI工厂 sqfliteFfiInit(); databaseFactory databaseFactoryFfi; runApp(const MyApp()); }之后创建数据库、建表、增删改查的代码和普通sqflite完全一致// 打开数据库 final db await databaseFactory.openDatabase( note_app.db, options: OpenDatabaseOptions( version: 1, onCreate: (db, version) async { await db.execute( CREATE TABLE notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, created_at INTEGER), ); }, ), ); // 插入 await db.insert(notes, { title: 训练营笔记, content: 今天学了本地持久化, created_at: DateTime.now().millisecondsSinceEpoch, }); // 查询 final notes await db.query(notes, orderBy: created_at DESC);这里有个细节数据库文件的路径也需要通过getDatabasesPath()来获取但在OpenHarmony上getDatabasesPath()默认返回的路径可能不是你预期的位置。我在真机上调试时发现最好显式指定一个完整路径比如用path_provider拿到应用文档目录后拼上数据库文件名这样路径可控、排查方便。2.3 文件存储路径千万别写死文件存储适用于图片缓存、日志文件、需要导出的文档。Flutter里拿目录的标准方式是通过path_provider插件在OpenHarmony上同样适用但返回的路径结构和Android略有不同。具体用法import package:path_provider/path_provider.dart; import dart:io; final dir await getApplicationDocumentsDirectory(); final file File(${dir.path}/user_cache.json); await file.writeAsString(jsonEncode(cacheData));常见的坑有三个第一不要自己拼接固定路径。OpenHarmony的应用沙箱路径在不同版本、不同设备上可能不同一旦写死换台设备就崩。务必通过path_provider动态获取。第二getTemporaryDirectory()拿到的临时目录可能被系统随时清理只能用来放“丢了也无所谓”的数据。如果你用临时目录存用户重要的配置那就是给自己埋雷。第三大文件写入时记得用分片或者流式写入别一次性readAsString()加载几个GB的文件到内存这个在后面的内存优化章节会细说。文件存储和数据持久化结合的一个典型场景是把网络返回的Json数据缓存成文件下次启动先读缓存再走网络更新。这种“缓存优先”策略在弱网环境下体验提升非常明显。训练营里布置的作业就有一个是让学员实现一个“新闻列表缓存”要求是列表页启动时先展示上一次的缓存数据后台拉取新数据后刷新并覆盖缓存实测下来用文件存储比用数据库更简洁因为数据本身就是一个大JSON字符串没必要拆成表结构。3. 手把手做一个“打卡笔记”Demo打通完整存储链路3.1 工程准备与依赖引入光讲原理容易飘训练营第13天下午就是纯实操大家现场做了一个“打卡笔记”Demo把前面讲的几种持久化方案全部串起来。这里我把整个步骤完整复现一遍你可以跟着走。首先创建工程。如果你还没有配置好OpenHarmony的Flutter开发环境需要先准备一套适配OpenHarmony的Flutter SDK和对应的DevEco工具链。这里有个重要的工程管理建议不同项目可能要求不同的Flutter版本强烈建议用FVMFlutter Version Management来管理多版本Flutter不要手动改环境变量。我见过太多人因为全局Flutter版本切换导致依赖报错用FVM之后这类问题基本绝迹。工程创建好后在pubspec.yaml中添加依赖dependencies: flutter: sdk: flutter shared_preferences: ^2.2.0 sqflite_common_ffi: ^2.3.0 path_provider: ^2.1.0 path: ^1.8.0然后执行flutter pub get。如果拉取依赖时出现网络超时或版本解析失败可以检查一下Flutter SDK的镜像配置以及pub缓存目录这个问题在OpenHarmony开发环境里特别常见因为部分依赖源在国内的访问速度不稳定。OpenHarmony侧的工程结构不需要手动创建Habm包适配后的Flutter工程会自动生成基于DevEco的OpenHarmony壳工程位于ohos目录下。你只需要在DevEco Studio里打开这个目录就能像开发原生鸿蒙应用一样进行构建和调试。3.2 实现配置存储SharedPreferences封装“打卡笔记”需要一个配置项来记录用户是否开启了“每日提醒”这里正好用SharedPreferences实现。我习惯把配置项封装成一个单例类这样业务层调用起来非常简洁import package:shared_preferences/shared_preferences.dart; class AppConfig { static AppConfig? _instance; static SharedPreferences? _prefs; AppConfig._(); static FutureAppConfig getInstance() async { if (_instance null) { _instance AppConfig._(); _prefs await SharedPreferences.getInstance(); } return _instance!; } Futurevoid setDailyReminder(bool enabled) async { await _prefs?.setBool(daily_reminder_enabled, enabled); } bool get dailyReminderEnabled _prefs?.getBool(daily_reminder_enabled) ?? false; }有一个细节必须强调AppConfig.getInstance()是异步方法在main()里使用时要保证Flutter引擎已经初始化完毕。最稳妥的做法是void main() async { WidgetsFlutterBinding.ensureInitialized(); final config await AppConfig.getInstance(); runApp(MyApp(config: config)); }如果忘记了ensureInitialized()在部分平台上会出现“Binding has not yet been initialized”的错误这个问题在OpenHarmony上同样存在训练营里就有学员踩到了。3.3 实现笔记列表的SQLite增删改查接下来是核心功能笔记的增删改查。这里选择用sqflite_common_ffi因为它在OpenHarmony上运行得很稳定。先建一个数据库工具类import package:sqflite_common_ffi/sqflite_ffi.dart; import package:path/path.dart as p; class NoteDatabase { static Database? _db; static FutureDatabase get instance async { _db ?? await _initDb(); return _db!; } static FutureDatabase _initDb() async { final dbPath p.join(await _getDbDir(), note_app.db); return databaseFactory.openDatabase( dbPath, options: OpenDatabaseOptions( version: 1, onCreate: (db, version) async { await db.execute( CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at INTEGER NOT NULL ) ); }, ), ); } static FutureString _getDbDir() async { // 这里用path_provider获取应用文档目录 final dir await getApplicationDocumentsDirectory(); return dir.path; } }注意_getDbDir()里我用了getApplicationDocumentsDirectory()这个函数来自path_provider。在OpenHarmony上它对应的就是应用沙箱内的文档目录不需要额外申请权限。这一点和Android上需要处理存储权限的逻辑不同对开发者来说省了不少心。然后写笔记的增删改查方法class NoteRepository { Futureint addNote(String title, String content) async { final db await NoteDatabase.instance; return db.insert(notes, { title: title, content: content, created_at: DateTime.now().millisecondsSinceEpoch, }); } FutureListMapString, Object? getNotes() async { final db await NoteDatabase.instance; return db.query(notes, orderBy: created_at DESC); } Futureint updateNote(int id, String title, String content) async { final db await NoteDatabase.instance; return db.update( notes, {title: title, content: content}, where: id ?, whereArgs: [id], ); } Futureint deleteNote(int id) async { final db await NoteDatabase.instance; return db.delete( notes, where: id ?, whereArgs: [id], ); } }这段代码基本就是标准的sqflite写法不需要因为OpenHarmony做特殊改动。实战中有一个小技巧如果是批量插入大量笔记可以用db.batch()把操作打包成一个事务提交速度能快不少而且任何一个失败会自动回滚数据一致性有保障。3.4 在OpenHarmony模拟器和真机上验证Demo写完后训练营安排大家在模拟器上先跑再到真机上验证。这里有一个很重要的验证动作重启应用确认数据还在。在OpenHarmony模拟器上运行方式很简单——DevEco Studio里选中entry模块点击Run即可。首次构建可能会很慢因为需要把Dart代码编译成鸿蒙侧可执行的形态卡在“Gradle Build Running”超过几分钟都是正常的。训练营里有人在构建时遇到报错You are applying Flutters main Gradle plugin imperatively using the apply script method这个问题的根因是Flutter的Gradle插件声明方式和新版Android Gradle Plugin不兼容解决办法是在android/settings.gradle里改用plugin management方式声明而不是用老的apply方式。这个坑在OpenHarmony工程里也出现过建议直接按Flutter官方模板重新生成工程然后把你自己的代码拿进去比手动修改Gradle配置要快得多。真机验证时要特别关注两点一是数据库文件路径是否正确可以打印出完整路径后在设备文件管理里实际查看二是SharedPreferences在App重启后是否真的持久化成功。我在真机上遇到过一次写入了数据但重启后取出来是null最后发现是因为我用了一个旧版本的自定义鸿蒙SDK底层的Preferences服务没有正确初始化。后面升级到最新适配版本就正常了所以如果遇到类似诡异问题先检查SDK和适配插件版本。4. 训练营里最常出现的6个坑与排查实录4.1 “画面渲染异常/白屏”往往不是存储代码的问题训练营里好几个学员在写完持久化代码后运行起来发现界面突然白屏或渲染花屏第一反应是存储写崩了。实际上OpenHarmony上的Flutter渲染是基于自家图形栈的和Android的SurfaceFlinger逻辑不完全一样。在部分低版本鸿蒙设备上Flutter首帧渲染会出现偶发的画面撕裂或白屏这大概率是图形渲染栈与GPU驱动之间的兼容性问题跟数据存储没有直接关系。排查思路很清晰先注释掉所有持久化相关代码只保留一个最简单的页面看是否还会渲染异常。如果问题依旧就说明是渲染链路的问题。临时解决方案是在ohos工程里调整硬件加速开关比如尝试开启软件渲染根治方案是升级OpenHarmony SDK版本或Flutter适配版本。另外一个经验是不要在build()方法里做任何耗时操作比如同步读取大文件、执行数据库查询这会严重阻塞帧管线表现起来同样是“画面卡死”。正确的做法是把耗时操作放到async方法里等数据准备好后通过setState刷新界面。4.2 Isolate间数据读写互相看不到前面提到过SharedPreferences在同一个Isolate内有缓存。训练营的打卡笔记应用加了“后台同步笔记”的功能用compute或者Isolate.run()在子Isolate里处理数据结果发现子Isolate里无论怎么读取都是旧值或空值。原因很直接每个Isolate都有独立的Dart堆内存你在主Isolate里调用SharedPreferences.getInstance()拿到的实例并不会被复制到新Isolate里。新Isolate需要重新执行getInstance()从底层存储重新加载数据。所以正确的写法不是把SharedPreferences对象传给子Isolate而是在子Isolate里重新初始化后再读取。如果是数据库注意sqflite_common_ffi默认也是不支持多个Isolate共享同一个Database实例的子Isolate要重新打开数据库连接。实操心得是涉及Isolate的持久化场景最好把数据操作收敛到一个独立的“存储服务”层无论主Isolate还是子Isolate都通过这个服务来读取数据不要在业务代码里到处直接操作数据库或Preferences。4.3 路径写死导致DirectoryNotFoundException文件存储最容易翻车的地方就是路径。有人为了省事直接在Dart代码里写final file File(/data/storage/el2/base/note.json);这种写法在Android上还能勉强跑但在OpenHarmony上路径结构完全不同而且不同系统版本的应用沙箱路径还在变化。一旦路径不存在File.writeAsString()或者Directory.create()就会抛出异常而且是那种运行一段时间才暴露的异常特别阴。正确姿势是永远用path_provider获取标准目录然后在标准目录下用path包拼接final dir await getApplicationDocumentsDirectory(); final notesDir Directory(p.join(dir.path, notes)); if (!await notesDir.exists()) { await notesDir.create(recursive: true); } final file File(p.join(notesDir.path, note.json));还有一个容易忽略的地方p.join和直接用字符串拼接结果一样但它能自动处理路径分隔符的问题在Windows、Linux、OpenHarmony上都不会出错。养成用path包的习惯跨平台开发会省很多事。4.4 异步初始化顺序导致崩溃这个问题是典型的“启动即崩溃”类型。场景是这样的你的App有A、B两个页面B页面读取数据库数据但你是在A页面点击跳转时才初始化NoteDatabase.instance。如果用户启动App后非常迅速地点击到B页面数据库可能还没初始化完成于是查询报DatabaseException: database_closed。解决方案有两种。一是“懒加载加锁”在第一次访问时统一初始化并且用单例模式保证线程安全。二是“启动预加载”在main()里就提前把数据库打开把初始化工作前置void main() async { WidgetsFlutterBinding.ensureInitialized(); await NoteDatabase.instance; // 提前初始化数据库 runApp(const MyApp()); }第二种方式看似多花了一点启动时间但换来的是后续任何页面访问都不会因为初始化时序出问题在训练营里我们统一采用这种方式。类似地SharedPreferences也建议在进入页面路由之前完成getInstance()调用。4.5 数据量大时的内存优化打卡笔记用久了数据库里的记录会膨胀。如果你一次性getNotes()把所有记录全部查出来内存占用会随着数据量线性增长页面卡顿、掉帧都来了。这里分享两个实际用过的优化手段第一个是分页加载。给查询加上LIMIT和OFFSET每次只加载20条配合ListView的滚动加载体验会流畅很多。代码很简单final page await db.query( notes, orderBy: created_at DESC, limit: 20, offset: pageNum * 20, );第二个是字段裁剪。如果列表页只需要显示标题和创建时间就不要把content字段查出来final page await db.query( notes, columns: [id, title, created_at], orderBy: created_at DESC, limit: 20, offset: pageNum * 20, );这个优化看起来微不足道但当单条记录content很长时节省的内存非常可观。同理SharedPreferences里不要存可以随时重建的大对象比如把整个网络响应Json直接塞进去——这种数据更适合用文件缓存。4.6 工程级坑构建工具链与Gradle声明方式最后说几个和持久化关系不大、但训练营里几乎每期都会出现的工程问题。第一个是Windows环境下构建报错Unable to find suitable Visual Studio toolchain。这个错误跟OpenHarmony关系不大而是Flutter Windows桌面构建需要Visual Studio的C工作负载。如果你的电脑同时装了Flutter Windows SDK和OpenHarmony SDK很容易触发这个检查。解决方案很简单在Visual Studio Installer里勾选“使用C的桌面开发”工作负载或者临时切到只依赖OpenHarmony工具链的构建路径不去触发Windows桌面构建。第二个是前面提到的Gradle插件声明方式报错。Flutter新版在android/settings.gradle里默认使用plugins闭包声明插件而老项目模板里用的是apply方法。如果你把老项目升级到新版Flutter SDK就会遇到这个报错。按官方模板重建Android壳工程然后把android/app/src/main里的代码复制过来是最省事的方法。第三个是热重载后状态异常。在OpenHarmony的Flutter适配里热重载Hot Reload对Dart层逻辑的更新很灵敏但如果你改了原生侧代码比如自己写的MethodChannel实现热重载不会生效必须重新编译完整工程。很多“为什么我改了没反应”的问题都是因为这个。提示排查这类问题我习惯性先看ohos目录下的日志输出再在Dart层加debugPrint分两步定位。先确认Dart方法有没有被执行再跟踪到原生通道有没有返回。大部分“存储不生效”的问题实际都卡在了“方法根本没调到原生层”这一步。这两天的训练营内容结束后我个人的体会是在OpenHarmony上做Flutter持久化核心难点不是Dart代码怎么写而是你要能理解不同平台存储能力的边界。SharedPreferences、sqflite这些名字在Android、iOS、OpenHarmony上看起来都一样但背后的沙箱规则、缓存策略、底层实现完全不同。每一次“莫名其妙的数据丢失”或者“路径找不到”几乎都能在“平台差异”上找到答案。最后再分享一个小技巧给持久化相关的所有键名加一个统一前缀。比如主题配置用app_theme缓存版本用app_cache_version数据库表名统一用t_notes而不是notes。这样做的好处是当你需要做数据迁移、清理无用缓存、或者排查线上数据问题时一眼就能看出哪些数据是当前版本在用的哪些是历史遗留。训练营后续几天的内容会涉及数据迁移和分布式同步这个命名习惯到时候能帮你省下不少事。