1. 从零搭建:Flutter for OpenHarmony 文件存储与数据库的整体思路
1.1 为什么存储和数据库在 OpenHarmony 上不能照抄 Android 经验
先说结论:Flutter for OpenHarmony 这套东西,UI 渲染层跟 Android 上差别不大,真正让人头皮发麻的恰恰是文件存储和数据库这些“跑在系统底层”的能力。我自己是从 Android 应用开发转过来搞鸿蒙适配的,一开始的想法特别天真——不就是 path_provider 拿个目录,然后 sqflite 开个库吗?结果第一周就被现实教育了。
问题出在几个地方。第一,官方 Flutter SDK 根本不认识 ohos 这个平台,你必须用 OpenHarmony SIG 维护的 Flutter fork 版本,否则连工程都创建不出来。第二,pub.dev 上那些主流插件大多是按 Android/iOS 原生接口写的,直接塞进 OpenHarmony 工程里,虽然能编译过去,但运行时要么路径拿不到,要么直接报 MissingPluginException。第三,OpenHarmony 的应用沙箱目录模型跟 Android 的 /sdcard 逻辑完全不一样,你不摸清 el1/el2 那套区分,文件一旦写到系统认为“可清理”的区域,用户重启个设备或者清个缓存,数据就没了。
所以这篇指南不是教你怎么写 Flutter,而是讲清楚在 OpenHarmony 上做本地存储时要避开的那些雷:环境怎么搭、沙箱目录怎么选、数据库到底用 sqflite 还是鸿蒙原生 relationalStore、以及为什么你折腾了半天 RK3568 设备树,应用依然在真机上跑不起来。适合谁看?准备把 Flutter 应用迁移到 OpenHarmony 真机或模拟器的开发者、负责应用适配的客户端工程师,还有那些刚拿到一块开发板、正被“选哪个 dtb”折磨得睡不着的人。
1.2 方案选型:文件存储、内嵌数据库和原生能力怎么配合
我一直主张一个原则:先想清楚数据要活多久、要不要跨设备、能不能丢,再选存储方案。别看数据库功能强大,就什么都往里塞。下面这个表是我在实际项目里反复对比后沉淀下来的选型逻辑,你可以直接抄。
| 数据类型 | 推荐方案 | 理由 |
|---|---|---|
| 日志、导出文件、图片缓存 | dart:io + path_provider_ohos |
简单直接,不引入额外依赖 |
| 设置项、用户偏好 | shared_preferences_ohos / Hive |
Key-Value 读写快,天然适配配置类数据 |
| 结构化业务数据、需要 SQL 查询 | sqflite_common_ffi 或社区 sqflite_ohos |
CRUD 成本低,迁移路径成熟 |
| 需要系统备份、多端协同的数据 | 平台通道调鸿蒙 relationalStore |
原生能力,和系统深度绑定 |
| 图片、音视频等大对象 | 文件系统存实体,数据库只存路径元数据 | 避免数据库体积暴涨、性能劣化 |
这个表背后有一个很痛的教训。我第一个版本图省事,把用户上传的图片直接 Base64 塞进 SQLite 的 BLOB 字段,结果表只有几千行,数据库文件已经涨到 300 多 MB,查询越来越慢,最后只能写迁移脚本把所有大对象导出来换成文件路径。所以你在设计表结构的时候,一定要把“大字段只存路径”当成铁律,不要心存侥幸。
另外,很多从 Android 迁移过来的朋友会下意识地写 getExternalStorageDirectory(),觉得这就是外置 SD 卡路径。在 OpenHarmony 上这么写,得到的很可能是 null 或者一个跟媒体库绑定、行为跟预期完全不同的路径,这也是后面常见问题里我会重点展开的地方。先把这些概念理清,后面实操才不会翻车。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:把 Flutter 跑在 OpenHarmony 真机上的第一步
2.1 获取带 ohos 平台的 Flutter SDK
不要用 flutter.dev 下载的官方 SDK 去做 OpenHarmony 工程,它没有 ohos 这个 platform,执行 flutter build ohos 会直接告诉你平台不支持。正确做法是拉取 OpenHarmony SIG 维护的 flutter_flutter 仓库,这个仓库就是专门适配 OpenHarmony 的 Flutter SDK 分支。
bash复制# 拉取带 ohos 支持的 Flutter SDK,具体分支/tag 以仓库 Releases 页面为准
git clone https://gitee.com/openharmony-sig/flutter_flutter.git -b <选择与你 OpenHarmony 版本匹配的 tag>
# 把 SDK 的可执行文件加入环境变量
export PATH="$PWD/flutter_flutter/bin:$PATH"
# 确认版本,顺便检查有没有遗漏的依赖
flutter --version
flutter doctor -v
这里有一个关键点:fork 版本的版本号要跟你的 OpenHarmony 系统版本匹配。比如你的开发板烧的是 API 10 的系统,却拉了一个只支持 API 12 的 Flutter fork,构建阶段大概率会报 SDK 版本不匹配。我建议先确定设备烧录的 OpenHarmony 版本,再去仓库 Releases 页挑对应 tag,不要在版本上凑合。真正的主题是存储和数据库,但环境这一步过不去,后面全是白搭。
还需要装 DevEco Studio 或者至少是配套的命令行工具链,因为最终打包、签名、安装到真机,走的是鸿蒙的 hvigor 那套构建体系,不是 Gradle。首次构建时,Flutter 会自动调用 hvigor 编译 ohos 目录下的工程,所以 DevEco 的命令行工具能正常执行是硬前提。
2.2 PATH 环境变量与“新终端才能生效”的坑
这个坑看起来小,但能把新手卡半小时。很多人照着文档 export PATH=... 了,然后在同一个终端窗口里执行 flutter,提示还是 command not found,就开始怀疑 SDK 没下全,其实只是 shell 没有重新加载环境变量。
解决方式很简单:要么重新开一个终端窗口,要么手动执行 source ~/.zshrc(如果写在别的配置文件里就对应用 source 那个文件)。Windows 用户更常见的是在 PowerShell 里用 setx 写了环境变量,当前窗口不会立刻生效,必须新开窗口。
bash复制# Linux / macOS
export PATH="$PWD/flutter_flutter/bin:$PATH"
source ~/.zshrc
which flutter # 确认能找到
# Windows PowerShell 里先确认
Get-Command flutter
这个事儿看着不起眼,但我见过不少群里求助的人,最后发现就是没开新终端。顺带提一句,如果 flutter doctor -v 检查出 CMake 或者 Visual Studio 组件缺失,在下一个小节里也会遇到,因为只要你的项目里有一个带原生代码的插件,构建时就会调 CMake。
2.3 初始化工程与验证基本通路
环境变量没问题之后,创建一个带 ohos 平台的工程,流程跟创建 Android 工程几乎一样,只是多了一步 flutter config --enable-ohos。
bash复制flutter config --enable-ohos
flutter create --platforms ohos demo_app
cd demo_app
flutter build ohos --debug
构建产物一般会输出到 build/ohos 下面,然后用 DevEco Studio 打开项目里的 ohos 目录,配置好签名之后安装到真机。如果你用的是 x86 架构的 OpenHarmony 模拟器或者 PC 版镜像,构建时要注意二进制产物架构匹配,真机通常走 ARM64,模拟器可能是 x86_64,flutter build ohos --debug --target-platform ohos-x64 这类参数按实际情况传。我在一个 x86 模拟器上折腾过半天,App 一直安装失败,最后发现就是目标架构传错了。
第一次跑起来之后,先别急着写业务逻辑。建议在 main 里打印几个关键的目录路径:getApplicationDocumentsDirectory()、getTemporaryDirectory()、getApplicationCacheDirectory(),亲眼确认它们指向哪。这一步能帮你建立对这个系统沙箱模型的直觉,也为后面的文件存储实操打底。
3. 文件存储实操:搞懂沙箱目录,读写才算入门
3.1 鸿蒙应用沙箱目录到底长什么样
OpenHarmony 的应用沙箱目录跟 Android 那种“应用私有目录 + 共享外部存储”的双层模型不一样,它的核心概念是加密级别,路径上最常见的开头是 /data/storage/el2/base。el 应该是 Encryption Level 的缩写,不同加密级别代表不同的解锁条件。
| 目录方法 | 常见映射路径 | 用途建议 |
|---|---|---|
getApplicationDocumentsDirectory() |
/data/storage/el2/base/files/documents |
用户可导出/可见的文档 |
getApplicationSupportDirectory() |
/data/storage/el2/base/files |
应用私有数据 |
getApplicationCacheDirectory() |
/data/storage/el2/base/cache |
可随时清掉的缓存 |
getTemporaryDirectory() |
/data/storage/el2/base/temp |
临时文件,系统可回收 |
getExternalStorageDirectory() |
可能为 null,视版本和权限而定 |
别当 Android 外置存储用 |
注意,上面的映射表是我根据常见 SDK 版本整理的经验值,不同 API 版本可能有差异,最靠谱的做法还是真机上打印路径。你自己跑一下会比任何文档都准。
另外一个必须理解的区别是 el1 和 el2。简单说,el2 跟用户解锁状态强相关,应用常规数据放这里没问题;el1 是设备级别的加密区域,适合放那些“设备开机后、用户还没解锁时也需要读取”的数据,普通应用基本用不上。你要是把用户隐私数据放到 el1,还容易被安全评审盯上,所以别乱迁。
3.2 用 path_provider_ohos 定位目录
官方 path_provider 在 OpenHarmony 上没有对应实现,需要换成社区适配的 path_provider_ohos。加依赖的方式跟普通插件一样,在 pubspec.yaml 里声明,然后 flutter pub get。
yaml复制dependencies:
# 版本号以 pub.dev 上实际能拉到的最新版为准
path_provider_ohos: ^1.0.0
代码里 import 的包名还是 path_provider_ohos,接口跟官方版本保持一致,这让老 Flutter 开发者几乎零学习成本。
dart复制import 'package:flutter/foundation.dart';
import 'package:path_provider_ohos/path_provider_ohos.dart';
Future<void> printAllPaths() async {
final documents = await getApplicationDocumentsDirectory();
final support = await getApplicationSupportDirectory();
final cache = await getApplicationCacheDirectory();
final temp = await getTemporaryDirectory();
debugPrint('documents: ${documents.path}');
debugPrint('support: ${support.path}');
debugPrint('cache: ${cache.path}');
debugPrint('temp: ${temp.path}');
}
我实际开发中会封装一个 StoragePaths 单例,在启动时一次性把这些路径读出来缓存住,避免每次读写都跨通道异步获取一次。虽然单次开销不大,但文件读写频繁时,模式化封装能省很多心理负担,也方便将来统一改根目录。
3.3 文本与二进制文件读写示例
拿到目录之后,文件读写其实就是纯 dart:io 的活儿,不需要任何额外插件。下面这段代码演示了怎么优雅地写一个文本文件,并把父目录一起创建出来。
dart复制import 'dart:io';
Future<File> getNoteFile(String fileName) async {
final documents = await getApplicationDocumentsDirectory();
final noteDir = Directory('${documents.path}/notes');
await noteDir.create(recursive: true);
return File('${noteDir.path}/$fileName');
}
Future<void> writeNote(String fileName, String content) async {
final file = await getNoteFile(fileName);
// 追加模式写入日志
await file.writeAsString(content, mode: FileMode.append);
}
Future<String> readNote(String fileName) async {
final file = await getNoteFile(fileName);
if (!await file.exists()) {
return '';
}
return file.readAsString();
}
有几个细节值得强调。第一,Directory.create(recursive: true) 很重要,因为 File.writeAsString 不会帮你自动创建父目录,不写这一句,首次运行时直接 FileSystemException。第二,如果业务流程是反复写同一个文件、要求写入过程中不出现半截文件,比如配置文件的持久化,建议先写临时文件,再 rename 覆盖目标文件,这个操作在文件系统层面是原子的,能避免应用被杀导致配置损坏。
二进制文件也类似,只是把 writeAsString 换成 writeAsBytes。OpenHarmony 的设备存储基本都是闪存,频繁小文件写入依然有寿命问题,所以涉及频次高的状态数据,尽量用下面的数据库方案,而不是动不动就落盘文本。
3.4 缓存清理与用户数据安全
沙箱目录不是无限大的,尤其是 cache 和 temp 区域,系统在存储压力大的时候随时可能清理。所以你的业务逻辑不能假设“这个文件写进去了,下次启动就一定能读到”。我的习惯是:用户主动产生的数据放 documents,派生缓存放 cache,并且定期做年龄清理。
dart复制Future<void> cleanCache({Duration maxAge = const Duration(days: 7)}) async {
final cache = await getApplicationCacheDirectory();
final dir = Directory(cache.path);
if (!await dir.exists()) return;
await for (final entity in dir.list(recursive: true, followLinks: false)) {
if (entity is File) {
final stat = await entity.stat();
final age = DateTime.now().difference(stat.modified);
if (age > maxAge) {
await entity.delete();
}
}
}
}
清理缓存是一件看似简单实则容易误删的操作。注意 followLinks: false,避免碰到符号链接时跟着跳出去删到不该删的地方。另外,用户数据安全这块,我的建议是敏感数据不要以明文形式躺在 support 目录里。OpenHarmony 虽然有应用沙箱隔离,但设备 root 或者漏洞利用场景下,明文数据依然是风险点。配合鸿蒙的密钥管理能力做字段级加密,或者用合规的加密数据库,是有必要的。
最后提醒一个容易误解的现象:很多人发现“文件删除后存储空间并没有立刻释放”,就开始怀疑系统有问题。在 Android 上这是回收站和媒体库索引在捣乱,在 OpenHarmony 的沙箱里直接 File.delete() 一般会立刻释放空间;但你如果走的是媒体库接口写入的图片、音频,删除之后记得刷新媒体库索引,否则界面显示还在,实际文件已经没了,反过来也会造成“看着在了,却读不出来”的错乱。
4. 数据库操作实操:从 sqflite 到鸿蒙原生 relationalStore
4.1 内嵌数据库三选一:sqflite、Hive 还是原生 RDB
做 Flutter 的人对 sqflite 再熟悉不过,但到了 OpenHarmony,整个选择就复杂了。我这里把主流的几条路线摆出来,你自己对号入座。
| 方案 | 优点 | 缺点 |
|---|---|---|
sqflite_common_ffi + sqlite3 |
纯 Dart 初始化,不依赖 Android/iOS 插件通道,逻辑最统一 | sqlite3 动态库需要自己适配 OpenHarmony 架构 |
社区 sqflite_ohos 适配包 |
接口接近原版 sqflite,迁移成本低 |
维护活跃度不确定,功能可能滞后 |
Hive |
性能高,适合对象和 KV 存储,纯 Dart 实现 | 不适合复杂 SQL 查询 |
平台通道 + relationalStore |
鸿蒙原生能力,支持系统备份、多端协同 | 需要写 ArkTS 原生代码,工程量最大 |
如果项目对查询复杂度要求高、又不想写原生代码,我推荐在 OpenHarmony 上用 sqflite_common_ffi 这条路线。原因是它把 SQLite 引擎通过 FFI 直接绑到 Dart 层,不需要走 Flutter 的 platform channel,减少了消息通道的开销,也避开了许多插件适配不一致的坑。前提是你要有一个能在 OpenHarmony 上加载的 libsqlite3.so,可以自己交叉编译,也可以找社区已经打好的产物放在 jniLibs 对应目录里。
如果你的数据需要跟鸿蒙的系统级备份、多设备协同联动,那就别绕了,直接走平台通道调 relationalStore,这才是“根正苗红”的方案。
4.2 平台通道调鸿蒙原生 relationalStore
平台通道的方案分两步。第一步,Dart 侧定义一个 MethodChannel;第二步,在鸿蒙侧的 UIAbility 里注册 handler,处理具体的数据库调用。
dart复制import 'package:flutter/services.dart';
class RdbService {
static const _channel = MethodChannel('dev.example/rdb');
static Future<int> insertNote(String title, String content) async {
final id = await _channel.invokeMethod<int>('insertNote', {
'title': title,
'content': content,
});
return id ?? -1;
}
}
鸿蒙侧的 ArkTS 代码大致是这样,核心是拿到 RdbStore 之后建表、插入,再把结果回传给 Dart。
typescript复制import relationalStore from '@ohos.data.relationalStore';
let rdbStore: relationalStore.RdbStore | undefined;
function initRdb(context: Context) {
const config: relationalStore.StoreConfig = {
name: 'app.db',
securityLevel: relationalStore.SecurityLevel.S1,
};
relationalStore.getRdbStore(context, config, (err, store) => {
if (err) {
console.error(`getRdbStore failed, code=${err.code}, message=${err.message}`);
return;
}
rdbStore = store;
store.executeSql(
'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, updated_at INTEGER)'
);
});
}
这段代码里最容易踩的坑是 Context 的获取。在 EntryAbility 里,你用 this.context;在普通页面组件里,你可能需要用 getContext(this) 才能拿到。我当时第一次写,直接在 Page 里用了 this.context,编译没报错,运行时直接空引用,排查了半天。另外 executeSql 是异步的,建表动作要放在 getRdbStore 的回调里,不能在回调外面立刻执行查询,否则表还没建好就插数据,会报 no such table。
平台通道的价值在于把鸿蒙原生的能力完整暴露给 Flutter。你一旦学会了这个套路,不只是数据库,支付拉起、图库选择、扫码这些原生能力都可以照葫芦画瓢接入 Flutter 侧。这也是 Flutter 应用在 OpenHarmony 上解决“官方插件没有适配”问题的通用方法论。
4.3 sqflite 纯 Dart 路线的完整示例
如果你决定走 sqflite_common_ffi,核心就是先初始化,然后把它当作标准 sqflite 用。
yaml复制dependencies:
sqflite_common_ffi: ^2.3.0
path_provider_ohos: ^1.0.0
Dart 侧初始化和 CRUD 完整示例。
dart复制import 'package:sqflite_common_ffi/sqflite_ffi.dart';
void initDatabaseFactory() {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
Future<Database> openDatabase() async {
final dir = await getApplicationDocumentsDirectory();
final dbPath = '${dir.path}/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, content TEXT, updated_at INTEGER)',
);
},
),
);
}
Future<void> insertNote(Database db, String title, String content) async {
await db.insert('notes', {
'title': title,
'content': content,
'updated_at': DateTime.now().millisecondsSinceEpoch,
});
}
Future<List<Map<String, Object?>>> queryNotes(Database db) async {
return db.query('notes', orderBy: 'updated_at DESC');
}
Future<void> deleteNote(Database db, int id) async {
await db.delete('notes', where: 'id = ?', whereArgs: [id]);
}
事务处理也很重要。批量插入或者“先删后插”这种组合操作,一定要包在 transaction 里。
dart复制await db.transaction((txn) async {
await txn.delete('notes', where: 'id = ?', whereArgs: [oldId]);
await txn.insert('notes', {...});
});
事务的好处是:中间任何一步失败,整个操作回滚,不会出现数据库里残留一半数据的情况。我见过有人用循环 insert 同步几十条数据,中途失败后数据库里多了半批脏数据,后来排查半天才意识到是事务的锅。
4.4 数据库升级与迁移策略
数据库一旦上生产,版本升级就是绕不开的课题。sqflite 的机制是:每次打开数据库,对比 version 和现有的 user_version,如果大了,就触发 onUpgrade。所以在 OpenDatabaseOptions 里写好 version 和 onUpgrade 是规定动作。
dart复制options: OpenDatabaseOptions(
version: 2,
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, updated_at INTEGER)',
);
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute('ALTER TABLE notes ADD COLUMN category TEXT DEFAULT ""');
}
},
),
升级脚本的原则是“增量前进,不破坏旧数据”。能用 ALTER TABLE 加列就别重建表;非要重建表的话,先创建新表,把旧数据 INSERT INTO ... SELECT ... 迁过去,再删除旧表,整个过程也放在事务里,保证中途失败能回滚。最忌讳的是在 onUpgrade 里写一段依赖当前时间或者外部状态的逻辑,数据库升级又不是实时计算,越确定越好。
4.5 进阶:本地库 + 后端同步需要提前设计的东西
很多项目做到后面都要面对“本地数据库 + 后端同步”的诉求,这也是网上特别热的搜索方向。我的经验是,这个能力必须在建表那一刻就埋好伏笔,否则后期改造非常痛苦。
同步的基本盘是给每张表增加两个字段:updated_at 时间戳和 deleted 标记。删除记录不要硬删,改成软删除,否则服务端不知道你这里删了什么。同步时,客户端把 updated_at > 上次同步时间 的数据上送,服务端返回增量数据,客户端再 upsert 到本地。冲突策略最简单的版本是“后写覆盖先写”,也就是拿最新 updated_at 作为赢家;更复杂的业务再做字段级合并。
关于大文件的同步,我的体会是“先传文件,再更新数据库状态”。如果你先把数据库记录标记成成功,然后文件传输失败,就会出现“列表里有这条数据,点开却没有内容”的尴尬局面。反过来,先传文件,成功了再更新 DB,失败就留着一个待重试的标记,这样至少数据是一致的。
5. 常见问题与排查技巧实录
5.1 RK3568 设备树那么多,到底选哪个
网上关于 OpenHarmony 和 RK3568 的问题,十个里有三个是在问设备树。原因很简单,RK3568 的开发板型号五花八门,而 OpenHarmony 内核源码里同一个芯片可能挂着十几份 .dts/.dtb,新手根本不知道烧录时选哪个。
设备树不是“选”出来的,而是跟板子硬件配置一一对应的。你手上是哪块开发板,就应该用哪份 dtb。比如大禹 200(RK3568 EVB)这类板子,常见的设备树文件是 rk3568-evb1-ddr4-v10.dtb 这一系列,但同样用 RK3568 的其他板子,DDR 型号、屏幕参数、外设引脚可能完全不同,强行用别家的 dtb 刷进去,常见的症状是启动黑屏、网口不通、触摸没反应。
几个靠谱的排查方法:
- 看 u-boot 的环境变量
fdtfile,很多板子从这里指定要加载的 dtb 文件名。 - 在已经跑起来的系统里执行
cat /proc/device-tree/model,直接告诉你当前加载的是什么板子型号。 - 如果系统把 dtb 放在
/boot目录,可以ls /boot/*.dtb看看有哪些可选。
更关键的一点是:在 OpenHarmony 的构建体系里,设备树的选择通常是在板级产品配置里指定的,不是应用层能改的。你刷的是官方发布的烧录包,就用包内自带的 dtb;你拿到的是裸板,要做的是按板子规格去移植和编译 dts,而不是祈祷“多试几个 dtb 总有一个能开机”。我建议先确认板卡厂商提供的支持文档,把 fdtfile 和实际硬件对上,再决定后续能不能跑 Flutter 应用,否则应用装上了也会因为驱动不对各种诡异闪退。
5.2 依赖版本错乱导致包拉不下来
Flutter 项目最经典的崩溃现场之一,就是 flutter pub get 卡住不动,或者报一堆版本解析冲突。在 OpenHarmony 项目里,这个问题更容易出现,因为你要同时兼容 pub.dev 上的通用 Dart 包和 _ohos 后缀的适配包,两者之间的传递依赖很容易打架。
我常用的排查路径是:
- 删掉
pubspec.lock和.dart_tool,执行flutter clean && flutter pub get,先排除残留缓存导致的假象。 - 确认关键依赖有没有锁版本。
_ohos适配包经常跟着上游 SDK 更新,你不锁版本,某天重新拉取就升级到不兼容的版本。 - 用
flutter pub get --verbose看具体卡在哪个包、哪个源,而不是干等。 - 如果网络波动导致下载失败,可以配置合法的镜像源,通过
PUB_HOSTED_URL和FLUTTER_STORAGE_BASE_URL调整包的下载地址,这能显著缓解依赖拉取不稳定的问题。
版本问题还有一个隐藏场景:你用的 Flutter fork 版本比较老,而 pub.dev 上最新插件要求较高的 Dart SDK constraint,于是 pub get 直接拒绝解析。这时候别硬升插件,看看该插件的旧版本是否满足你的 SDK 版本,或者找插件在 OpenHarmony 社区的 fork。硬升级 Flutter SDK 来解决依赖,往往会引发新一轮适配问题,收益不划算。
5.3 Windows 下构建报 CMake / Visual Studio 生成器错误
在 Windows 上构建 OpenHarmony 工程,只要项目里有一个带原生代码的插件,构建系统就会调 CMake。很多人的报错长这样:CMake Error at CMakeLists.txt:3 (project): Generator Visual Studio ...。第一反应往往是代码问题,其实是你机器上缺少匹配的 Visual Studio 组件,或者 CMake 没找到合适的生成器。
解决办法分两步。第一步,安装 Visual Studio Build Tools,勾选“使用 C++ 的桌面开发”工作负载,这一步会把 CMake、MSVC 编译器、Windows SDK 都装上。第二步,在“x64 Native Tools Command Prompt”环境里执行构建,让 CMake 能找到正确的编译器和生成器。如果你已经装了完整版 VS 还报错,检查 flutter doctor -v,它会直接告诉你缺什么。
这个坑很耽误时间,我建议在环境准备阶段就跑一遍 flutter doctor -v,把红叉全部清掉再开始写代码。等项目做了两周再来补环境,心态会非常崩。
5.4 文件删除后存储不释放怎么办
这个问题的搜索量一直很高,典型描述是“在平板上删了文件,但设置里看存储占用没变”。先说结论:大多数情况下不是系统 bug,而是删除操作没有落到你想要的位置。
在 Android 上,很多文件管理器删除文件只是把它移进回收站,或者媒体库索引没刷新,所以空间不释放。在 OpenHarmony 的沙箱里,直接用 File.delete() 删除后,空间一般是即时释放的;但如果你用的是媒体库接口写入的文件,删除后索引没刷新,界面仍显示文件存在,存储空间也看起来被占着,这时候需要手动刷新媒体库缓存。
还有一种常见情况是:你的应用把文件写进了 cache 或 temp 目录,系统有延迟清理机制,空间并不是立刻回收到“可用空间”里。遇到这类问题,别急着怀疑系统,先确认文件到底删在哪一层目录、是不是走了媒体库接口,再决定要不要手动触发缓存清理。
最后,把常见问题做个速查表,方便你直接定位。
| 现象 | 一句话解法 |
|---|---|
flutter build ohos 提示平台不支持 |
确认用的是 OpenHarmony SIG 的 Flutter fork |
path_provider 报 MissingPluginException |
换 path_provider_ohos 并重新构建 |
relationalStore 查询报 no such table |
建表放到 getRdbStore 回调里执行 |
| 文件读出乱码或路径不存在 | 先打印真实目录路径,别猜 |
| 构建报 CMake generator 错误 | 安装 VS C++ 工具链,用 Native Tools 终端 |
| 包拉不下来 | 锁版本、清缓存、配置镜像源 |
| 删除文件空间没变化 | 确认是否走了媒体库,刷新索引 |
6. 个人心得与最后一点建议
这几周把 Flutter 应用往 OpenHarmony 上跑,最大的感受是:这个生态还在快速变化中,很多东西没有现成答案,最可靠的排错方式是把问题拆到最小单元去验证。比如文件存储,先写一个只打印目录路径的 demo,确认路径对了再谈读写;数据库也一样,先建一张空表,insert 一条数据,能查出来,再往上摞业务逻辑。别看这些步骤简单,能帮你把“环境问题”和“代码问题”干净地切开。
还有一个建议是养成先看 Release 日志的习惯。OpenHarmony 适配包更新很快,你今天踩的坑,可能在上一个版本的 changelog 里已经写了。遇到诡异问题,先查 pubspec.lock 里实际锁到的版本,再查对应版本的发布说明,比在网上漫无目的地找答案高效得多。另外,目录映射、hardcode 的路径这些信息,不同 SDK 版本可能不一样,一定要以真机上的实际输出为准,不要迷信任何一篇博客,包括我这一篇。
最后再分享一个小技巧:把存储和数据库相关的操作全部收口到独立的 service 层,不要散落在页面代码里。等 OpenHarmony 的适配包升级、路径规则变化时,你只需要改一个文件,而不是满工程找 File( 和 db.。这个项目后续如果要接真正的多端协同,或者把底层换成更统一的数据库引擎,有了这一层抽象,改动成本也会小很多。
