家里老人的药箱永远一团乱:降压药和感冒药混在一起,过期三个月的阿莫西林还躺在最上层。我决定自己做一个小工具来解决这个问题——一个跑在OpenHarmony设备上的家庭药箱管理App。选型时没有太多犹豫:系统层用OpenHarmony,应用层用Flutter,第一个核心功能就从药品列表开始做。
Flutter for OpenHarmony不是谷歌官方直接支持,而是OpenHarmony SIG维护的Flutter引擎适配分支。好处很直接:Dart代码、Widget树、状态管理、pubspec依赖这些Flutter生态的东西基本都能复用,未来如果想把同一套药箱逻辑搬到手机、平板甚至Windows桌面上,UI部分几乎不用重写。现在OpenHarmony上的原生应用生态确实还在爬坡阶段,很多组件、三方库要么没适配,要么比较粗糙,而Flutter把UI渲染、导航、动画、文本排版这些最烧时间的部分都替你处理好了。
这篇内容我会从选型思路、环境搭建、数据模型、药品列表UI、鸿蒙能力调用,到真机调试时遇到的几个典型问题,一条线讲完。适合手里有RK3568开发板、想在OpenHarmony上跑Flutter应用的朋友,也适合想用Flutter做工具类App但被环境适配劝退的人。
1. 为什么家庭药箱这类轻工具App适合用Flutter跑在OpenHarmony上
1.1 OpenHarmony应用开发现状与Flutter的切入点
先说结论:OpenHarmony要跑业务应用,目前有三条路。
第一条用ArkTS + ArkUI写原生应用,这是系统亲儿子,性能和系统能力调用最好,但问题在于生态太新。你想要的日历控件、图表库、数据库ORM、扫码组件,很多都要自己造轮子,或者从开源社区找半成品改。
第二条用W3C标准写类Web应用,开发快但不适合做需要流畅交互和本地数据库的工具类App,药箱管理这种需要频繁增删改查、列表滚动、状态切换的场景,Web套壳体验不够好。
第三条就是我选的Flutter。Flutter的渲染引擎是自绘的,不依赖系统WebView,也不依赖ArkUI组件库,所以只要OpenHarmony的适配层提供Surface和输入事件通道,Flutter就能跑起来。你写的ListView、Card、TextField、Hero动画,在OpenHarmony和Android上表现高度一致,不存在“换了个系统UI全变样”的问题。
家庭药箱这种App,说实话功能不复杂:药品列表、详情、有效期提醒、用药记录。但它的核心痛点在于“数据维护”和“列表呈现”。Flutter在这两块的优势恰好最强——声明式UI写列表非常顺手,SQLite数据层也成熟。你要在ArkUI里写一套同样流畅的药品列表,不是不行,但需要多花不少时间去补ArkUI的组件知识。
1.2 设备碎片化:RK3568设备树那么多,到底怎么选
OpenHarmony和Android有一个相似的问题:适配的设备很多,每块开发板的硬件配置都不一样。热搜里那句“openharmony的rk3565有许多设备树到底咋选”我太有体会了。RK3568是OpenHarmony社区最常用的开发板芯片之一,但同样是RK3568,开发板可能有不同的DDR配置、不同的显示接口(HDMI、MIPI-DSI、eDP)、不同的触摸屏型号,这些差异都会体现在设备树文件里。
如果设备树选错了,最常见的结果是系统能启动,但Flutter应用黑屏、花屏或者触摸没反应。为什么?因为Flutter的渲染依赖GPU和SurfaceFlinger,设备树里的display节点、GPU节点、iommu节点如果和实际硬件不匹配,引擎创建的Surface就是坏的,上层代码再正确也白搭。
我的经验是:不要自己去翻设备树源码一个个试,优先用你手上的OpenHarmony版本官方镜像里自带的、针对那块开发板的完整设备树。比如你用瑞芯微的RK3568 SDK跑OpenHarmony,就找发布说明里明确标注“RK3568标准版”的镜像。如果一定要自己编译修改,只动你确实需要改的部分,比如屏幕分辨率或触摸IC型号,GPU、显示控制器、内存映射这些节点别碰。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建:Flutter SDK、OpenHarmony SDK与DevEco Studio的三方配合
2.1 三条工具链各自扮演什么角色
在OpenHarmony上跑Flutter,不是装一个Flutter就能完事的,需要搞清楚三套东西各管什么。
Flutter SDK负责Dart编译、Flutter框架代码、pub依赖管理和flutter run命令;OpenHarmony SDK提供ArkTS编译工具链、Native API、资源编译以及最终打包HAP的能力;DevEco Studio则是一个集成开发环境,管理OpenHarmony工程、签名、设备连接和调试。
这三者之间有版本对应关系。flutter_for_openharmony的不同分支通常对应OpenHarmony的不同主版本,比如支持OpenHarmony 4.x的版本,在SDK上可能就有对应的release tag。我的建议是不要盲目追求最新版,先去你用的OpenHarmony SDK发布说明里找“Flutter适配版本”表格,按表格锁定版本再开始。装好之后,在工程目录下检查oh-package.json5和build-profile.json5里声明的SDK版本,跟你本机OpenHarmony SDK版本是否一致,不一致就改配置文件或者重新下载对应SDK。
VSCode能不能写?能,写Dart代码完全没问题。但最后构建HAP、签名、安装到开发板,还是得回到DevEco Studio里操作。我不建议在VSCode里硬扛,除非你只改Dart层代码、构建交给已有的DevEco工程。
2.2 Flutter安装和环境变量的坑:PATH要新终端才生效
Flutter装完之后,很多人的第一个报错是:
bash复制'flutter' is not recognized as an internal or external command
或者Linux/macOS上的:
bash复制flutter: command not found
搜索关键词里那句“flutter 刚装好,path 需要新终端生效”说的就是这个问题。修改完PATH之后,你当前已经打开的那个终端窗口不会自动刷新环境变量,必须重新打开一个终端窗口,或者手动执行source命令。Windows用户注意:只关掉当前标签页再开一个新标签页是不够的,某些终端软件会继承旧环境变量,建议直接退出终端程序重新打开。在macOS上执行:
bash复制source ~/.zshrc
Linux上如果是bash:
bash复制source ~/.bashrc
Windows上还有个细节:把Flutter的bin目录加到用户PATH而不是系统PATH。用户PATH不需要管理员权限,改完生效更快,也避免以后装其他工具时不小心动了系统PATH导致flutter命令失效。
配置完之后,建议先跑一遍:
bash复制flutter doctor
如果OpenHarmony相关的检查项没有显示出来,说明flutter_for_openharmony分支里那个带ohos的doctor检查没被触发,这时候检查一下你是不是真的用了适配OpenHarmony的Flutter SDK,而不是谷歌官方版本。
2.3 从flutter create到第一个OpenHarmony窗口
如果你用的flutter_for_openharmony适配版本支持ohos平台,创建工程是这样的:
bash复制flutter create --platforms ohos medicine_box
如果不支持这个参数,那就从flutter_flutter仓库的模板目录里把ohos模板复制到你的工程里,具体步骤以你拉取分支的README为准。
创建完工程后,目录结构大概长这样:
text复制medicine_box/
├── lib/
│ └── main.dart
├── ohos/
│ ├── entry/
│ │ └── src/main/
│ └── build-profile.json5
├── pubspec.yaml
└── android/
这里有个容易误解的点:工程里默认还会有android、ios这些目录,你不用管它们。只要在DevEco Studio里打开ohos目录,让它作为OpenHarmony工程来构建就行。如果你完全不需要Android侧,把android目录删掉可以避免后续一些Gradle插件报错,具体见后面踩坑部分。
连接开发板之后,在DevEco Studio里完成签名配置,然后点击运行。第一次跑起来会慢一点,因为要把Flutter引擎和你的Dart代码整体编译打包进HAP。如果看到终端里出现:
text复制Flutter run key commands.
h: Toggle help.
说明Flutter引擎已经在OpenHarmony设备上跑起来了,接下来就是正常的热重载开发流程。
3. 家庭药箱的数据模型:药品列表的核心字段与本地数据库设计
3.1 药品列表该存哪些字段
药品列表是整个药箱App的地基,字段设计如果拍脑袋,后期加需求会很痛苦。我最终的medicines表字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | TEXT | UUID主键,不用自增整数 |
| name | TEXT | 药品通用名 |
| spec | TEXT | 规格,如“0.25g*24片” |
| dosage | TEXT | 用法用量,如“每次1片,每日3次” |
| category | TEXT | 分类:处方药/OTC/保健品 |
| expire_date | TEXT | 有效期,ISO格式yyyy-MM-dd |
| stock_count | INTEGER | 当前库存数量 |
| location | TEXT | 存放位置,如“客厅药箱” |
| image_path | TEXT | 药品图片本地路径 |
| remind_enabled | INTEGER | 是否开启提醒,0或1 |
| remind_time | TEXT | 提醒时间,HH:mm格式 |
| create_time | TEXT | 创建时间 |
| update_time | TEXT | 最后修改时间 |
主键用UUID而不是自增整数,是因为家庭药箱未来很可能要支持多端共享。老人手机上一份数据,你的手机上一份数据,如果大家都用自增ID,同步时必然产生主键冲突。UUID虽然多占几个字节,但对药箱这种量级的数据毫无压力。
过期时间不要存时间戳,直接用“2026-03-14”这种ISO格式文本。SQLite对TEXT类型的比较是按字典序的,ISO格式的日期字符串天然满足时间先后顺序,ORDER BY expire_date ASC就能得到正确的过期排序。
3.2 数据库选型:sqflite还是drift
Flutter里做本地数据库,最常见的是sqflite和drift。对OpenHarmony来说,我的建议是优先用sqflite。原因很简单:flutter_for_openharmony社区对sqflite的适配相对成熟,API和标准sqflite基本一致,网上能找到的大多数sqflite教程直接就能抄。drift功能更强,有类型安全的查询生成器,但它依赖sqlite3原生库和更多代码生成环节,在OpenHarmony上的适配和调试成本明显更高。家庭药箱这种规模的项目,根本用不到drift的高级特性。
依赖配置:
yaml复制dependencies:
sqflite: ^2.3.0
path: ^1.9.0
如果标准sqflite在OpenHarmony上编译不过,社区通常有对应的适配包,包名可能是sqflite_ohos之类,API用法保持一致。遇到这种情况别慌,把包名换掉,import语句改成适配包的路径就行。
sqflite本身是纯本地数据库。热搜里有“flutter 做本地数据库+后端同步”的需求,我建议不要指望数据库层自动同步,本地库就用SQLite存数据,同步逻辑单独写一个Repository层,在增删改操作后把变更上报到后端。这样数据库选择只聚焦在本地,复杂度低很多。
3.3 数据库初始化与建表
我在DatabaseHelper里做数据库初始化:
dart复制import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
DatabaseHelper._();
static final DatabaseHelper instance = DatabaseHelper._();
static const _dbName = 'medicine_box.db';
static const _dbVersion = 1;
Database? _database;
Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, _dbName);
return openDatabase(
path,
version: _dbVersion,
onCreate: _onCreate,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE medicines (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
spec TEXT DEFAULT '',
dosage TEXT DEFAULT '',
category TEXT DEFAULT 'OTC',
expire_date TEXT,
stock_count INTEGER DEFAULT 0,
location TEXT DEFAULT '',
image_path TEXT DEFAULT '',
remind_enabled INTEGER DEFAULT 0,
remind_time TEXT DEFAULT '',
create_time TEXT NOT NULL,
update_time TEXT NOT NULL
)
''');
await db.execute(
'CREATE INDEX idx_medicines_expire_date ON medicines(expire_date)',
);
await db.execute(
'CREATE INDEX idx_medicines_name ON medicines(name)',
);
}
}
索引的作用在数据量上去之后会非常明显。药品列表按过期日期排序,expire_date索引能让排序和筛选都不走全表扫描。name索引则是为了搜索时加速WHERE name LIKE查询。
这里有一个开发中容易踩的坑:数据库升级。如果第二版增加了字段,不能只在onCreate里改建表SQL,因为onCreate只在数据库文件第一次创建时执行。你还要在onUpgrade里做ALTER TABLE迁移:
dart复制onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute('ALTER TABLE medicines ADD COLUMN location TEXT DEFAULT \'\'');
}
}
药箱App可以不上云,但本地数据迁移的规范还是要有的,不然以后加字段就只能卸载重装,数据全丢。
4. 药品列表页实现:从仓库到UI的一条龙
4.1 Repository层与状态管理
数据库操作不要直接在UI里写。我会在中间加一个MedicineRepository,它负责把DatabaseHelper的原始SQL映射成Medicine模型对象,UI层只跟Repository打交道。
dart复制class MedicineRepository {
Future<List<Medicine>> getAllMedicines() async {
final db = await DatabaseHelper.instance.database;
final rows = await db.query(
'medicines',
orderBy: """
CASE
WHEN expire_date < date('now') THEN 0
ELSE 1
END,
expire_date ASC
""",
);
return rows.map(Medicine.fromMap).toList();
}
Future<List<Medicine>> searchMedicines(String keyword) async {
final db = await DatabaseHelper.instance.database;
final rows = await db.query(
'medicines',
where: 'name LIKE ? OR spec LIKE ?',
whereArgs: ['%$keyword%', '%$keyword%'],
orderBy: 'expire_date ASC',
);
return rows.map(Medicine.fromMap).toList();
}
Future<void> insertMedicine(Medicine medicine) async {
final db = await DatabaseHelper.instance.database;
await db.insert('medicines', medicine.toMap());
}
}
状态管理方面,我用的Provider + ChangeNotifier。对于药品列表这种规模,Riverpod也不差,但没有必要。setState倒也能跑,一旦加上搜索、分类、排序这几个交互,setState会很快变得臃肿。用一个MedicineListViewModel持有列表数据和加载状态,界面就清爽很多。
4.2 药品卡片UI:让过期状态一眼可见
药箱App的核心场景不是“好看”,而是“快速判断这个药能不能吃”。所以列表项的视觉层级必须突出过期状态。
我设计了三种状态:
- 正常:白色背景,黑色文字
- 即将过期(30天内):左侧色条橙色,显示“剩余X天”
- 已过期:卡片整体浅红色,显示“已过期X天”
计算剩余天数的函数:
dart复制int daysUntilExpiry(DateTime expireDate) {
final today = DateTime.now();
final expiry = DateTime(expireDate.year, expireDate.month, expireDate.day);
final current = DateTime(today.year, today.month, today.day);
return expiry.difference(current).inDays;
}
这里要注意一个细节:不能用DateTime.now()直接和expireDate做difference,因为DateTime.now()带时分秒,你早上打开App和晚上打开App,计算结果会差一天。必须把两个时间都归一化到当天零点。
UI层核心结构:
dart复制ListView.builder(
itemCount: medicines.length,
itemBuilder: (context, index) {
final medicine = medicines[index];
final daysLeft = daysUntilExpiry(DateTime.parse(medicine.expireDate));
return MedicineCard(
medicine: medicine,
daysLeft: daysLeft,
onTap: () => _openDetail(medicine),
);
},
)
MedicineCard内部就是一个Card + Padding + Row,左侧放药品信息和规格,右侧放库存数量和有效期状态标签。Icon和装饰都不要加太多,药箱场景只求信息密度和可读性。
4.3 搜索、分类与排序的SQL优化
搜索框用TextField,输入时实时刷新列表。防抖还是要做的,不然每敲一个字母都要查一次数据库,速度虽然不至于卡,但浪费资源。最简单的防抖:
dart复制Timer? _debounce;
onChanged: (value) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 300), () {
_viewModel.loadMedicines(keyword: value);
});
}
分类筛选不需要单独写三个方法,Repository里加一个category参数就行。排序规则我在查询SQL里用CASE表达式把“已过期”压到最前面,然后是即将过期的,最后是正常药品。为什么不拿到内存里再排序?因为数据库做这件事只需要一次文件扫描和排序,内存排序也不慢,但药箱数据量再大也撑不起让用户感知到的区别,SQL更清晰,一次到位。
搜索、筛选、排序三者组合起来,SQL的where条件就要动态拼接:
dart复制Future<List<Medicine>> queryMedicines({
String? keyword,
String? category,
}) async {
final db = await DatabaseHelper.instance.database;
final conditions = <String>[];
final args = <Object?>[];
if (keyword != null && keyword.isNotEmpty) {
conditions.add('(name LIKE ? OR spec LIKE ?)');
args.addAll(['%$keyword%', '%$keyword%']);
}
if (category != null && category.isNotEmpty) {
conditions.add('category = ?');
args.add(category);
}
final where = conditions.isEmpty ? null : conditions.join(' AND ');
return db.query(
'medicines',
where: where,
whereArgs: args,
orderBy: """
CASE
WHEN expire_date < date('now') THEN 0
ELSE 1
END,
expire_date ASC
""",
).then((rows) => rows.map(Medicine.fromMap).toList());
}
动态拼接SQL时最容易出SQL注入的地方,就是keyword直接拼进字符串。用?占位符传参,这不仅是安全性问题,也是避免引号导致语法错误的基本操作。
5. 结合OpenHarmony特性的扩展:条码扫描、提醒通知与图库取图
5.1 Platform Channel:Dart调用鸿蒙原生能力的方式
药品列表做出来之后,最自然的下一步是加三个系统级能力:扫药品条码添加、到期通知提醒、从图库选药品图片。这三个能力Flutter插件在OpenHarmony上不一定都适配好了,所以你必须掌握Platform Channel这条通用路子。
和Android的MethodChannel用法类似,Dart侧定义一个通道:
dart复制import 'package:flutter/services.dart';
class MedicinePlatform {
static const _channel = MethodChannel('com.medicine_box/scanner');
static Future<String?> scanBarcode() async {
try {
return await _channel.invokeMethod<String>('scanBarcode');
} on MissingPluginException {
return null;
}
}
}
OpenHarmony侧需要在EntryAbility的初始化阶段注册对应的MethodCallHandler。具体API以你用的SDK版本为准,套路是拿到传入的MethodCall,判断method名,执行扫码逻辑,最后通过result返回给Dart侧。返回的条码内容可以直接用来拼接药品名称搜索,比如去数据库里查有没有同条码的历史记录。
这里的核心思路是:Flutter负责UI交互,OpenHarmony负责系统API。Flutter生态里像“flutter兼容鸿蒙拉起iap支付”这类需求,同样也是走Platform Channel到鸿蒙侧的IAP SDK,不能指望Dart层直接拉起支付。
5.2 用药提醒通知怎么落地
药品提醒有两大类:一类是“每天按时吃药”,另一类是“药品即将过期”。flutter_local_notifications在OpenHarmony上的适配成熟度不算高,我不建议在项目初期就依赖它。
更稳妥的方式是:在OpenHarmony的EntryAbility/对应Ability里使用系统通知接口,Flutter侧通过Platform Channel去触发。你只需把remind_time和remind_enabled存在本地数据库里,然后开发一个定时任务,到点调用通知接口。OpenHarmony通知通常需要用户在系统设置里同意应用发送通知,第一次触发时判断一下是否有通知授权,没有就引导用户去开启。
如果你不想碰原生通知,也可以先用App内的“打开App后弹提醒”做替代方案:启动时扫描当前时间匹配的提醒记录,在首页弹一个对话框。这虽然不是真正的系统通知,但对药箱这种低频工具App,初期已经完全够用。
5.3 药品图片:从系统图库取图和沙箱存储
热搜里那句“flutter如何调用鸿蒙的图库”很典型。在OpenHarmony上,图片选择器涉及文件权限和沙箱规则,比Android复杂。你依然有两种选择:找image_picker的OpenHarmony适配版,或者自己写Platform Channel调用系统相册。
如果自己写,Dart侧调用:
dart复制static const _pickerChannel = MethodChannel('com.medicine_box/picker');
final String? imagePath = await _pickerChannel.invokeMethod<String>('pickImage');
鸿蒙侧返回的不是全局路径,而是一个应用可以访问的临时文件副本。拿到这个路径后,把它拷贝到应用的沙箱目录里,数据库里只存这个沙箱路径。直接保存原路径是典型的坑:系统清理临时目录后图片会丢,而且其他应用无权访问。
注意:读取图库需要申请ohos.permission.READ_IMAGEVIDEO等权限,并且需要在module.json5里声明。如果只是临时借用系统Picker选择一张图,有些系统版本不要求声明,但要考虑不同版本兼容性,还是提前声明更省心。
6. 真机调试踩坑记录:从依赖拉取到黑屏的完整排查
6.1 依赖包下载失败与版本错配:先看锁文件
热搜里“flutter各个版本不对导致依赖包下不下来”这个现象,几乎每个Flutter开发者都遇到过。报错五花八门,根源通常有两个。
第一是Flutter SDK版本与pubspec.lock不匹配。一个项目在3.10版本上生成的lock文件,拿到3.24版本里执行flutter pub get,某些包会要求更新约束,你以为是网络问题,其实是版本约束冲突。解决方法是删除pubspec.lock再重新拉取,或者执行:
bash复制flutter pub upgrade
第二是网络环境导致的拉取超时。国内开发者可以用镜像环境变量,这个操作不复杂:
bash复制export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
Windows PowerShell:
powershell复制$env:PUB_HOSTED_URL = "https://pub.flutter-io.cn"
$env:FLUTTER_STORAGE_BASE_URL = "https://storage.flutter-io.cn"
设完之后重新打开终端,再执行flutter pub get。如果还是拉不下来,检查一下是不是项目里的某个依赖最新版本要求更高的Dart SDK版本,顺手把Dart SDK版本报错也补齐。
6.2 Gradle插件报错:you are applying flutter's main gradle plugin imperatively
有些Flutter工程里还保留着android目录,即使你目标是OpenHarmony,在某些操作下还是会看到这个报错:
text复制You are applying Flutter's main Gradle plugin imperatively using the apply method, which is no longer supported...
这个问题根因是Flutter新版模板把Gradle插件声明方式改了。旧模板在android/settings.gradle里用apply方法,新模板要求用pluginManagement的plugins块声明。
如果你不需要构建Android版本,最省事的方案是直接把工程根目录下的android文件夹删掉。删掉之后,DevEco Studio构建ohos目录时不受影响,问题彻底消失。如果还需要保留Android构建能力,那就把android/settings.gradle改成新语法:
gradle复制plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
}
这个坑告诉我们:flutter_for_openharmony工程虽然是OpenHarmony为主,但默认脚手架是混着来的,你得明确自己到底要不要构建Android侧,不要两边都稀里糊涂地维护。
6.3 RK3568真机黑屏:一次从日志到根因的排查链路
这个坑最有代表性。现象:在Android模拟器和OpenHarmony模拟器上,药箱列表跑得好好的,换到一块RK3568开发板上,应用启动后黑屏,没有任何崩溃弹窗。
我的排查链路是这样的:
第一步,看进程在不在。执行:
bash复制hdc shell ps -ef | grep medicine_box
进程存在,说明Dart VM正常启动了,不是JVM崩溃。
第二步,看Flutter引擎日志。在DevEco Studio的Log面板过滤“flutter”关键字,发现循环刷一条关于Surface创建失败的日志。关键词大概是“Failed to create render surface”之类。
第三步,对比不同设备。同一份HAP在另一个型号的开发板上能正常显示,说明代码本身没问题。问题锁定在“当前这台RK3568的显示环境”。
第四步,回到设备树。我重新对比了烧录镜像的README,才发现这台开发板要求选择带GPU内存保留节点的那份设备树,而我烧的是通用最小配置,显示节点虽然能点亮系统桌面,但GPU Surface没被正确初始化,Flutter引擎拿不到适合渲染的Surface,于是黑屏。
第五步,重新烧录匹配的设备树镜像,应用启动正常。
这个坑的教训是:在OpenHarmony开发板上跑Flutter,不要只验证“系统能开机”就认为环境OK,一定要验证GPU渲染链路。可以先用系统自带的应用看桌面滑动是否流畅,再用Flutter的demo工程跑一次,确认渲染正常后再写业务代码。
6.4 中文乱码与数据库路径
药品名称如果出现中文乱码,先别怀疑数据库编码。SQLite本来就存UTF-8,一般不会出问题。真正的元凶往往是源代码文件编码不对。Windows上如果编辑器默认保存成ANSI编码,Dart文件里带中文串,编译时可能不报错,但运行时UI里就是乱码。所有源代码文件统一保存为UTF-8 without BOM,问题立刻消失。
数据库路径这块,OpenHarmony应用沙箱目录和Android的/data/data不同。如果你硬编码类似“/data/data/...”这样的路径,在OpenHarmony上要么拿到的是应用专属目录的另一个挂载点,要么直接没有权限。正确做法是通过getDatabasesPath()或者path_provider插件获取系统分配的沙箱路径,然后在那下面创建数据库文件。不要自己拼路径,因为你不知道这台设备的用户ID、包名、沙箱挂载规则。
最后再分享一个小技巧
药箱列表做出来后,我刚拿到真机上跑通的那一刻,最明显的感受是:Flutter在OpenHarmony上虽然能用,但不要默认所有Flutter插件都能直接工作。每引入一个依赖,先看一眼它有没有原生代码,有的话就要在OpenHarmony侧验证原生部分是否能编译。sqflite、path_provider这类基础库还好,图像识别、相机扫码这些就得做好自己写Platform Channel的准备。
另一个经验是,在开发初期就接入设备树匹配好的真机,不要只在模拟器上开发。很多OpenHarmony的显示和输入问题,模拟器完全复现不了。等到真机再查,可能要推翻部分UI设计,损失更大。
家庭药箱这个App目前已经能管理三百多条药品记录,列表滚动、搜索、过期标记都很流畅。下一步我打算把“拍照识别药品包装上的文字”加进去,但这条路可能又是一坑接一坑。如果你也在做Flutter for OpenHarmony,希望这篇内容能帮你少踩几个我踩过的坑。
