前阵子做了个项目,名字叫“flutter_for_openharmony口腔护理app实战+知识实现”。说实话,当初看到这个方向,不少人第一反应是“Flutter还能跑OpenHarmony?”,第二反应是“口腔护理做个App是不是太细分了”。这两个问题放到一起,反而让我觉得特别值得折腾:一边是跨平台框架往国产系统迁移的硬核适配,一边是护理场景里“知识库+自测+提醒”这种典型数据密集型应用,两者结合,几乎把移动开发的日常难点都摸了一遍。
这篇文章我尽量写得像一次完整的项目复盘,不是那种贴几个截图就完事的showcase。我会从技术选型、环境搭建、核心功能实现(知识库、自测逻辑、护理计划、本地通知)、一直讲到设备部署和问题排查,里面涉及的SQL、命令、报错解决方案,基本都是我实际跑过的。如果你也是Flutter开发者,正打算试试OpenHarmony方向,或者只是想把一个带本地数据库的中小型App做扎实,这篇文章应该能帮你少踩几个坑。
1. 项目概述与技术选型
1.1 为什么盯上Flutter与OpenHarmony这个组合
先说结论:OpenHarmony目前对Flutter的官方支持,主要由OpenHarmony SIG组维护,仓库叫flutter_flutter和flutter_engine,对应的Flutter SDK分支是OpenHarmony-5.0.x Release。这意味着你不需要完全抛弃现有Flutter代码库,只要换一套SDK分支,再适配一下工程配置,业务代码基本能保留七八成。
这个项目选择Flutter的原因很实际。口腔护理App的核心页面是知识文章、自测问卷、护理计划、打卡记录,这些界面在Android、iOS、OpenHarmony上要尽量保持一致。用ArkUI(OpenHarmony自研声明式UI)当然能做,但团队已有的组件库、状态管理方案、路由方案都是基于Flutter生态的,迁到ArkUI等于重写。而flutter_flutter这个分支的出现,让存量Flutter工程师可以用几乎不变的方式,把代码跑在OpenHarmony设备上。
我当时在DevEco Studio里分别建了ArkUI和Flutter的Demo工程对比:同样的口腔自测问卷页面,ArkUI大概要写300多行结构代码,Flutter用Column加ListView,100多行就能搞定,而且动画曲线、页面切换的体验明显更顺。这不是说ArkUI不好,而是在“跨端复用”“团队熟悉度”这两个维度上,Flutter方案对存量团队更友好。
所以最后的选型路线是:Flutter负责UI和业务逻辑,OpenHarmony负责系统能力承载(Ability、权限、通知),数据库用SQLite落本地。这个方案的好处是,后续如果要把App移植到Android或iOS,核心代码基本不用动,只需要处理平台相关的配置。
1.2 口腔护理知识体系如何结构化成数据
“知识实现”这四个字,是这个项目最容易被低估的部分。如果只是做几个文章页面,那Flutter里写死几个Article对象就够了。但口腔护理的“知识”不是单一维度的文章集合,它至少包含四层结构:
- 病因与症状对应:牙龈出血、口臭、牙齿敏感、龋齿,每类问题对应不同的护理建议和就医指征。
- 护理方法流程:巴氏刷牙法、牙线使用步骤、冲牙器选择、儿童涂氟时间表,这些内容适合用“步骤化”展示。
- 自测题体系:通过问卷初步判断用户口腔风险等级,再根据结果推送对应的知识文章。
- 护理计划与提醒:早晚刷牙、每半年洗牙、每季度更换牙刷,这些是典型的周期性计划。
这四层知识不能散落在JSON文件里,否则后续维护和联调都是灾难。我最后的方案是用SQLite做结构化管理,建了article、quiz、answer_record、care_plan四张表,文章正文用Markdown存储,在Flutter端做解析渲染。这样“知识”不再是死的静态页面,而是可以被自测结果、用户行为动态调度的数据。
1.3 本地优先的架构思路:隐私、离线、同步
口腔护理数据属于健康类敏感数据,用户对“上传云端”这件事天然有顾虑。再加上口腔App的使用场景往往是早晚刷牙时随手记一下,那时候手机网络未必稳定。所以这个项目我坚定采用“本地优先”架构:
- 所有文章、自测题、护理计划都从本地SQLite读取,首次启动时把asset目录下的预置数据库拷贝到应用沙箱。
- 用户的自测记录、刷牙打卡、护理完成情况先写本地库,UI立刻刷新。
- 预留一个sync_record表,记录每次本地操作的updated_at时间戳,后续接入后端时做增量同步即可。
这套设计的核心是“不给用户添堵”。本地库能解决的,不依赖网络;需要同步的,只传增量字段。后面做后端扩展时,只需要写一个同步manager,拉取远端变更合并到本地,再把本地变更推上去,不会影响现有功能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OpenHarmony开发环境搭建与工程初始化
2.1 工具链准备:DevEco Studio、Flutter SDK、镜像配置
先强调一个容易翻车的点:不能用官方flutter SDK直接跑OpenHarmony,必须用OpenHarmony SIG维护的分支。我用的版本是OpenHarmony-5.0.1-Release,对应DevEco Studio 5.0及以上。
bash复制# 克隆flutter的openharmony分支
git clone -b OpenHarmony-5.0.1-Release https://gitee.com/openharmony-sig/flutter_flutter.git
export PATH="$PWD/flutter_flutter/bin:$PATH"
# 配置国内镜像,否则依赖下载会拖很久
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://mirrors.huaweicloud.com/flutter
这里有个细节:环境变量配置完以后,如果是在已有终端窗口里直接执行flutter命令,经常会出现“flutter: command not found”,因为shell不会自动重读环境变量。重启终端,或者手动执行source ~/.bashrc / source ~/.zshrc,新配置才会生效。我一开始就是忘了这一步,白白浪费了十几分钟。
然后验证环境:
bash复制flutter doctor
正常情况下会看到OpenHarmony相关的检查项。如果没有,检查DEVECO_SDK_HOME环境变量是否指向DevEco Studio里的Sdk目录,或者手动在flutter config里指定SDK路径。
2.2 创建Flutter工程并适配OHOS
环境就绪后,创建工程和平常一样,只是platforms参数要加上ohos:
bash复制flutter create --platforms ohos --org com.example oral_care
生成的工程目录里,会多出一个ohos文件夹,这就是OpenHarmony的工程壳。注意:你在命令行里写业务逻辑(lib目录下的dart文件),但真正编译成hap包,需要用DevEco Studio打开ohos目录来编译签名。
工程创建后,我习惯先把ohos/app/src/main/ets/entryability里的Ability名称确认一下,这和后面的启动配置有关。另外,编译hap包需要签名,个人开发建议直接在DevEco Studio里配置自动签名,创建一个调试证书就行,不然真机装不上。
一个小建议:工程里的ohos目录不要手动去改太多东西,尤其是build-profile.json5和module.json5。这些文件由DevEco Studio管理,手动改容易导致签名配置失效。
2.3 真机部署与RK3568设备树选择
OpenHarmony最常见的开发板就是瑞芯微RK3568系列,典型的有DAYU200、DAYU210以及各种厂商的评估板。很多人在“设备树”(dts)这一步卡住——同样一颗RK3568,厂商给了十几种dts,到底选哪个?
我的经验是分三步:
- 先确认板子的具体型号和硬件版本。DAYU200还是DAYU210?扩展板是哪个型号?这些信息通常在开发板外壳标签或者官方文档里。
- 大多数情况下,用默认的dts就能跑。比如DAYU200开发板,烧录时选择rk3568-standard或dayu200相关配置,成功率最高。如果你只是调试App功能,不用纠结dts的细节。
- 如果运行后屏幕、触摸、网口异常,才需要根据外设型号换dts。比如触摸IC型号不一样,对应的dts节点就得改。
怎么确认当前设备信息?连上板子后用hdc shell查看:
bash复制hdc shell param get const.product.name
hdc shell dmesg | grep -i "machine"
然后直接用DevEco Studio的设备管理器连接板子,选择对应设备点Run。Flutter侧:
bash复制flutter run -d <deviceId>
这里的deviceId可以通过flutter devices查看。实测下来,只要设备树选对了,Flutter应用在板子上跑起来的流畅度还是可以的,毕竟RK3568是4核A55,跑个轻量护理App压力不大。
3. 口腔护理App核心功能实现
3.1 知识库的结构化存储与SQLite方案
口腔护理知识库不能每次启动都从网络拉,也不能把几十篇文章写死在代码里。我的方案是“数据库预置+首次拷贝”:
- 在pubspec.yaml里把oral_care.db放到assets目录。
- App首次启动时,检查应用沙箱里是否存在同名数据库文件,如果不存在,就从assets拷贝到getApplicationSupportDirectory()或getApplicationDocumentsDirectory()。
- 之后所有查询都走沙箱里的数据库,这样用户后续修改自测记录时,不会污染原始assets文件。
OpenHarmony上路径获取不能直接用path_provider的默认实现,需要搭载对应的OpenHarmony适配版本,或者通过ohos平台通道获取应用数据目录。我用的是path_provider_ohos插件,它把getApplicationSupportDirectory映射到了OpenHarmony的应用沙箱路径,和Android的files目录类似。如果不想引入额外插件,也可以自己在ohos侧写一个MethodChannel返回沙箱路径,但直接用适配插件更省事。
数据库表结构我设计成下面这样:
sql复制CREATE TABLE article (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
title TEXT NOT NULL,
summary TEXT NOT NULL,
content TEXT NOT NULL,
tags TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE quiz (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question TEXT NOT NULL,
options TEXT NOT NULL,
score TEXT NOT NULL,
analysis TEXT NOT NULL,
type TEXT NOT NULL
);
CREATE TABLE answer_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
quiz_id INTEGER NOT NULL,
selected_options TEXT NOT NULL,
score INTEGER NOT NULL,
advice TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE care_plan (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
title TEXT NOT NULL,
target_time TEXT NOT NULL,
repeat_rule TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1
);
options和score都存JSON字符串,这在Flutter侧解析很方便。article的content存Markdown,渲染时用flutter_markdown插件转成富文本。
这里有个经验分享:不要把知识的业务字段拆成太多张表。像“文章分类”这种,直接用category字符串就行,不必额外建category表。知识库的查询场景很固定(按分类列表、按标题搜索、按标签过滤),SQLite完全扛得住,过度设计反而增加维护成本。
3.2 口腔自测问答模块与评分逻辑
自测模块是整个App里用户参与度最高的功能。它的核心逻辑是:用户回答一组口腔健康相关问题,根据选项得分汇总,输出一个风险等级和个性化建议。
我设计了10道题,覆盖牙龈出血、牙齿敏感、口臭、牙结石、刷牙习惯、定期检查等维度。每道题的每个选项对应一个分数,比如“刷牙时牙龈出血”的选项“经常”给3分,“偶尔”给1分,“从不”给0分。
评分逻辑放在一个独立的dart类里:
dart复制class QuizResult {
final int totalScore;
final String level;
final List<String> adviceList;
QuizResult({required this.totalScore, required this.level, required this.adviceList});
}
QuizResult evaluateQuiz(List<Map<String, dynamic>> answers) {
int total = answers.fold(0, (sum, item) => sum + (item['score'] as int));
String level;
List<String> advice = [];
if (total <= 3) {
level = '口腔状态良好';
advice.add('继续保持早晚刷牙和使用牙线的习惯');
advice.add('建议每半年到一年洗牙一次');
} else if (total <= 8) {
level = '轻度风险';
advice.add('牙龈出血/敏感症状可能与刷牙方式有关,尝试巴氏刷牙法');
advice.add('建议查看知识库“牙齿敏感”专题');
advice.add('如果症状持续两周以上,建议预约牙科检查');
} else {
level = '高风险';
advice.add('建议尽快安排牙科就诊,进行专业检查');
advice.add('在就诊前,避免使用过冷过热刺激性食物');
advice.add('可以查看知识库“牙周炎”专题了解护理要点');
}
return QuizResult(totalScore: total, level: level, adviceList: advice);
}
这个逻辑简单直接,但效果很好。它把“知识库”和“自测”串起来了:高风险用户会被引导到“牙周炎”文章,低风险用户会被引导到“日常护理”文章。文章不再是孤立的,而是根据用户状态动态推荐。
自测记录每次都会写入answer_record表,首页展示一个“最近一次自测得分”的卡片,用户可以看到自己口腔风险的变化趋势。如果连续多次高风险,App会弹一个本地通知提醒用户及时就医,这个提醒用本地通知就能实现,不需要后端参与。
3.3 护理计划与本地提醒
口腔护理App没有提醒功能,基本就是个“科普阅读器”。我的目标是帮用户养成两个最小习惯:每天早晚各刷一次牙(至少2分钟),每周至少用3次牙线。再加两个周期性任务:每半年洗牙一次、每季度更换牙刷。
care_plan表里,repeat_rule字段存的是类似“每天两次”“每周一三五”“每季度”这种规则,我用一个枚举加字符串描述,没有引入复杂的日历库。本地提醒用flutter_local_notifications,OpenHarmony上的适配版本已经支持定时通知和每日重复通知。
实现上,我在首页放了一个“今日护理清单”:
- 早晚刷牙打卡:点击后记录当前时间,如果同一时间段已经打过卡,则提示“今日刷牙已记录”。
- 牙线使用打卡:记录使用时长,存一个简短的备注。
- 季度更换牙刷提醒:根据上次更换日期,超过90天就在任务卡片上显示“需要更换”。
提醒的触发逻辑:
dart复制Future<void> scheduleBrushingReminders() async {
await flutterLocalNotificationsPlugin.periodicallyShow(
1,
'口腔护理提醒',
'该刷牙啦,记得使用巴氏刷牙法,刷够2分钟',
RepeatInterval.daily,
const NotificationDetails(),
);
}
这里遇到一个坑:OpenHarmony的本地通知需要申请通知权限,并且部分版本上要配置通知渠道(NotificationChannel)。如果只调periodicallyShow而没配置渠道,真机上可能不弹通知。解决方案是在App启动时先初始化渠道:
dart复制const AndroidNotificationChannel channel = AndroidNotificationChannel(
'oral_care_channel',
'口腔护理提醒',
description: '日常护理提醒',
importance: Importance.high,
);
然后确保hasPermission()为true,否则弹通知会被系统静默拦截。这块我在“常见问题”里会再展开。
3.4 知识文章阅读体验优化
知识库页面最初我做的很简单,就一个ListView套WebView,后来发现体验太差:加载慢、样式跟App风格不统一、深色模式下刺眼。最后换成flutter_markdown解析本地Markdown,配合自定义样式,效果干净很多。
巴氏刷牙法这类内容,纯文字很难讲清楚。我用的是“步骤列表+配图轮播”的方案:
- 文章页顶部是标题和摘要,接下来是章节目录。
- 刷牙方法的正文拆成6个步骤,每个步骤配一张示意图,用PageView横向滑动查看。
- 文章底部是“相关自测题”和“相关护理计划”,点击直接跳转,形成闭环。
搜索功能我做了个很轻量的方案:用SQLite的LIKE查询标题和tags,不需要接搜索引擎。
dart复制Future<List<Article>> searchArticles(String keyword) async {
final result = await db.rawQuery(
'SELECT * FROM article WHERE title LIKE ? OR tags LIKE ? OR summary LIKE ?',
['%$keyword%', '%$keyword%', '%$keyword%'],
);
return result.map(Article.fromMap).toList();
}
实测下来,几百篇文章几毫秒就能返回结果,完全够用。知识库这个模块,关键不是技术有多花哨,而是内容组织得是否清晰、用户能不能在3步以内找到想看的东西。我最后是按照“问题分类”作为一级入口:牙龈出血、牙齿敏感、口臭、龋齿、儿童口腔、正畸护理六大类,每类下面挂文章和自测题。
4. 常见问题与排查技巧实录
4.1 环境与构建问题
问题1:flutter命令在刚安装完SDK后找不到。
原因基本是PATH没生效。新开的终端窗口不会自动加载你刚才export的路径。重新source环境配置文件,或者直接重启终端,基本都能解决。
问题2:不同分支、不同版本的Flutter混用,导致依赖包下载不下来。
这个挺常见的。如果你之前电脑上装的Flutter是stable分支,现在换成OpenHarmony分支,pubspec.lock里缓存的包解析路径可能会冲突。我建议在一个独立目录里维护OpenHarmony专用SDK,不要和常规Flutter SDK混在一个PATH里。切换时用一个配置文件管理环境变量,或者干脆用fvm来做SDK版本管理。
问题3:构建时遇到“You are applying Flutter's main Gradle plugin imperatively using the apply script”这类报错。
这个是因为Flutter 3.x之后的Gradle插件应用方式变了,不允许用旧式的apply script语法。解决方法是在android/ohos工程里改用插件DSL方式声明:
code复制plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application"
id "org.jetbrains.kotlin.android"
}
不要再用apply from: "$flutterRoot/packages/flutter_tools/gradle/app.gradle"这种写法。如果项目既有Android目录又有ohos目录,记得两边的gradle配置都检查一遍。
问题4:依赖下载慢或者卡死。
OpenHarmony工程里,部分Gradle和Maven依赖需要走镜像。在Flutter侧配置了PUB_HOSTED_URL和FLUTTER_STORAGE_BASE_URL之后,Gradle侧还需要在build.gradle里配置Huawei的Maven仓库镜像,具体地址以开源社区发布的为准。改完后清一下gradle缓存再重新同步。
4.2 UI与交互问题
问题1:CheckboxListTile文字距离按钮太近或太远,怎么调。
这个坑很多人问。CheckboxListTile的间距主要由contentPadding控制,默认是EdgeInsets.zero。想让文字离复选框远一点,可以设置:
dart复制CheckboxListTile(
contentPadding: EdgeInsets.only(left: 12, right: 12),
title: Text('使用牙线打卡', style: TextStyle(fontSize: 16)),
controlAffinity: ListTileControlAffinity.leading,
...
)
另外,如果想让复选框在右侧,把controlAffinity设成trailing即可。不要用Transform去硬调间距,内容容易被裁切。
问题2:Flutter Web上字体变小,在OpenHarmony上会不会也这样。
Flutter Web字体变小通常是因为默认字体栈不含中文字体,导致浏览器回退到小号默认字体。解决办法是在MaterialApp的theme里显式配置fontFamily,或者加载本地字体文件。OpenHarmony设备上也有类似情况,系统默认字体和Flutter默认字体不一致,建议在App里直接打包并加载思源黑体或鸿蒙字体,保证各端一致。
dart复制theme: ThemeData(
fontFamily: 'HarmonyOS_Sans_SC',
...
),
问题3:中文内容显示为方块或乱码。
先检查是否加载了对应字体文件,再检查fonts配置的family名是否和pubspec.yaml里定义的一致。字体文件别全都塞到assets/fonts根目录,建议按字重分目录,不然打包体积会失控。
4.3 兼容性与发布问题
问题1:Flutter App如何拉起鸿蒙生态的应用内支付(IAP)。
OpenHarmony的应用内支付走的是分发市场的IAP接口,与Android的Google Play Billing / 微信支付宝SDK都不一样。Flutter侧要做的是封装一个MethodChannel,在ohos侧用AVA(Ability)调用系统的IAP服务,支付结果回传Flutter。这个方案可行,但要注意:IAP支付必须在整包上架分发市场后才能完整测试,本地调试只能测支付取消和失败分支。我建议把支付能力抽象成一个服务接口,后续针对不同平台分别实现,不要让业务代码里到处是if (Platform.isAndroid)这类判断。
问题2:反编译Flutter App,核心逻辑会不会泄露。
这里想说一个容易被忽略的安全点。Flutter Release包虽然默认开启AOT编译,代码不是直接的JavaScript,但Dart生态里已经有工具可以对libapp.so做静态分析和符号还原。所以关键算法和敏感逻辑不要全放在客户端,尤其是涉及健康数据的脱敏规则、权限校验这类内容,建议放到服务端。客户端的知识库和自测数据并不需要高等级保护,但涉及用户身份和支付凭证的部分,一定要使用系统安全存储,不要把密钥硬编码在dart文件里。
问题3:用x86架构的电脑跑OpenHarmony模拟器,或者直接装x86版OpenHarmony,有什么要注意的。
如果你想在个人电脑上直接测试OpenHarmony系统,可以下载社区构建的x86_64镜像,跑在虚拟机里,或者直接安装到部分兼容硬件。这种方式适合快速看UI界面和基本交互,但有两个明显限制:一是无法完整验证OpenHarmony特有的系统能力和硬件相关服务,比如通知渠道在某些模拟器版本上可能不完整;二是性能比真机差不少,动画流畅度参考意义有限。我建议日常调试用模拟器,但每两个版本至少要上RK3568真机跑一遍。
5. 项目复盘与后续规划
整个项目做下来,我最想分享的一点是:Flutter for OpenHarmony并不是“能不能跑”的问题,而是“怎么把平台差异封装好”的问题。环境搭建其实不算难,真正花时间的,是把路径获取、通知权限、IAP支付这些系统能力逐一想清楚、适配好。
知识实现这个部分,我越做越觉得方向选对了。如果一开始只是写死几个页面,后面每加一篇新文章、每改一道自测题,都要发版,维护成本会非常痛苦。现在内容全在SQLite里,运营人员只要更新数据库文件,App下次启动时做一次增量检查就能拉取到新内容,这个效率不是改代码能比的。
最后再分享一个小技巧:口腔护理App的打卡模块,不要让用户填太多表单。我的实现是“一键打卡+默认时长”,用户点一下按钮就完成了整个记录动作。不要小看这个交互简化,它直接影响了用户能不能坚持使用。做健康护理类产品,功能再全,用户不愿意点,一切都白搭。后续我还想在这个基础上加入语音提醒、智能问答等能力,但核心原则不变:先把离线体验做扎实,再谈花活。
