☰
用Flutter开发OpenHarmony艺考题库App:从环境搭建到学习报告
2026/10/20 18:52:03 网站建设 项目流程

今年年初我接到一个需求,要在 OpenHarmony 设备上做一款艺考真题题库App,刷题、模考、错题本都要有,最关键的是要带学习报告功能。团队里没人碰过 Flutter for OpenHarmony 这套组合,网上能搜到的资料要么停留在跑通 Hello World,要么就是官方文档里语焉不详的适配说明,几乎没有一篇能告诉我“题库 + 统计报告”这种完整产品怎么落地。做完之后我觉得很有必要把全过程写下来——从环境搭建、数据模型设计,到学习报告的统计口径和图表实现,再到跨端适配时踩的一堆坑,给后面想用 Flutter 做 OpenHarmony 应用的同行留一份能直接抄作业的参考。这篇文章适合有 Flutter 基础、想拓到 OpenHarmony 平台的客户端开发者,也适合正在评估“题库类产品跨端方案”的产品和技术负责人。

1. 为什么选 Flutter 做 OpenHarmony 题库:需求背后的真实原因

1.1 艺考刷题这个场景,到底特殊在哪

艺考和普通升学考不一样,它不是一个“科目大盘子”,而是覆盖美术、音乐、舞蹈、播音编导等多个大类,每个大类下面还有细分方向的题库需求。我做这个项目时,甲方是某培训机构,他们的学生要刷的理论题主要是文艺常识、美术史、乐理、艺术概论、影视编导基础这几块。题型以单选题、多选题、判断题为主,偶尔掺杂一些填空题。

这带来三个特殊点:

  • 知识点标签是刚需。学生刷完题不是只看对错的,教务老师要能看出“这个班文艺常识普遍掌握得差”还是“乐理里面调式判断总是错”。所以题目数据模型里必须有 category 和 knowledgePoint 两个字段,学习报告全靠它们聚合。没有这两个字段,后面做报告就是空中楼阁。
  • 碎片化刷题场景多。艺考生的训练时间被素描、形体、发声练习切得很碎,刷题基本发生在通勤路上、候场间隙、睡前十分钟,离线可用是第一优先级,在线同步反而是其次。
  • 报告要给机构看。学习报告不只是学生自己的复盘工具,也是机构续课和教学调整的抓手。这意味着报告不能只给一个总正确率,得拆到知识点维度,最好还有时间趋势。

1.2 Flutter + OpenHarmony 的组合逻辑

这个项目的客户端预算是有限的,团队里没有专职的 ArkTS 原生开发。而 OpenHarmony 生态的设备,尤其是机构会统一采购的学习终端和平板,数量在明显增长,产品覆盖范围把 OpenHarmony 划进来了。

Flutter 在这里的价值是:一套 Dart 代码,同时出 Android 和 OpenHarmony 两个安装包。OpenHarmony 的 Flutter 支持来自开源社区的维护分支,纯 Dart 实现的 UI 和业务逻辑基本都能被这层适配接住,前提是别用太多平台插件。

我在技术选型阶段就定了一条原则:能直接搜到 OHOS 适配的插件才用,搜不到就用纯 Dart 方案或者自己写 MethodChannel,绝对不在关键路径上赌一个“据说能用”的插件。这条原则后面帮我们省了非常多排查时间。

2. 环境搭建从 0 到 1:版本选型和第一个 HAP

2.1 工具链和版本选型

Flutter for OpenHarmony 的 SDK 不能用官方 Flutter 发行版,得用 OpenHarmony 社区维护的 flutter_flutter 仓库的 ohos 分支。我落地时用的是基于 Flutter 3.7 系列的适配版本,这套 API 对做题库App完全够用。需要注意:

  • 不要混用官方 Flutter SDK,否则 build 时根本找不到 ohos 平台。
  • 需要配套的 OpenHarmony SDK,在 IDE 里下载对应 API 版本的 SDK。
  • 构建产物是 HAP 包,类似 Android 的 APK。调试期用 Debug 证书没问题,发布必须配置签名,这和 Android 的 keystore 逻辑类似。

工具链清单大致如下:

组件作用备注
flutter_flutter(ohos分支)Dart 侧 SDK 与编译工具通过 git 拉取,配置到 PATH
OpenHarmony SDK原生侧编译所需在 IDE 中配置路径
OpenHarmony 配套 IDE管理 ohos 工程、签名、构建 HAP打开项目下的 ohos 目录使用
ohpmOpenHarmony 的包管理器原生侧依赖安装

2.2 工程结构长什么样

Flutter 工程创建之后,除了常规的 android、ios 目录,还会多出一个 ohos 目录,这就是 OpenHarmony 的壳工程。核心结构是:

项目根/ ├─ lib/ # Dart 业务代码 ├─ ohos/ # OpenHarmony 壳工程 │ ├─ entry/ │ │ └─ src/main/ets/ # ArkTS 原生侧代码 │ ├─ AppScope/ # 应用级配置 │ └─ oh-package.json5 # 原生依赖声明 ├─ assets/ # 题目 JSON、图片等资源 └─ pubspec.yaml

大多数时候我们只需要在 lib/ 下写 Dart,ohos/ 目录里的东西不做大改。只有涉及原生能力调用时才需要动 ets 目录下的文件。

2.3 跑通第一个 HAP

我这边跑通工程的完整路径是这样:

  1. 把 ohos 分支的 Flutter SDK 下载到本地,配好 PATH,确认flutter --version能输出版本信息。
  2. 创建工程后,在项目根目录新建local.properties,写入ohos.sdk.dir=你的OpenHarmony SDK路径。这一步不写,后面flutter devices识别不到 OpenHarmony 设备。
  3. 连接设备或启动模拟器,执行flutter devices,能看到带 ohos 标识的设备条目。
  4. 直接flutter run -d 设备ID,首次构建会非常慢,因为要编译引擎相关的 so 文件,耐心等。
  5. 如果要出可安装的 HAP 包,把 ohos 目录导入配套 IDE,IDE 会跑 ohpm 安装原生依赖,同时帮你处理签名,再触发构建。

2.4 环境上踩过的几个坑

  • flutter doctor 一直报警。OpenHarmony 平台的工具链不会被完整识别,doctor 会反复提示 Android toolchain 缺失。别慌,这说明不了什么,只看实际构建能不能过。
  • Debug 能跑、Release 装不上。Release 包没配签名,设备上安装直接失败。解决方式是 IDE 里生成证书并配置到工程的 build-profile.json5 里。
  • 热重载是半个废的。纯 Dart 代码改动基本能热更新,但动了原生 ArkTS 或者引擎相关的东西,必须整个 HAP 重新构建,不能指望热重载救场。

3. 题库和刷题核心:数据模型、组卷与错题归集

3.1 题目数据模型

题目数据我设计成了一份 JSON 文件放进 assets,而不是一开始就塞进数据库。原因有两个:题目是只读数据,一套题库撑死几千条,全量加载进内存也就几兆;用 JSON + 启动时加载,能绕开 sqflite 这类数据库插件在 OpenHarmony 上适配不稳定的问题。

单题结构如下:

{ "id": "mc_00123", "type": 0, "category": "美术史", "knowledgePoint": "文艺复兴美术", "stem": "下列哪位艺术家被认为是“文艺复兴美术三杰”之一?", "options": ["达·芬奇", "鲁本斯", "伦勃朗", "委拉斯贵兹"], "answer": [0], "analysis": "文艺复兴美术三杰指达·芬奇、米开朗琪罗、拉斐尔。" }

type 用 0 单选、1 多选、2 判断,answer 用数组是为了兼容多选题。knowledgePoint 字段在学习报告阶段是聚合维度,所以导入时就必须按一套固定知识点词表清洗,不能随性乱填。这里的教训是:数据清洗比写代码费时间。我们当时拿到的原始题库是 Excel,里面“文艺复兴”一会儿写成“文艺复兴时期美术”,一会儿写成“文艺复兴艺术”,我写了段 Python 脚本按映射表统一替换,才保证了后面报告统计口径不出岔子。

3.2 刷题流程与错题归集

刷题页的状态机很简单:idle -> answering -> answered -> next。用户点选项时记录选中答案,点“下一题”时把这条答题记录写入本地库,同时更新当前会话的统计缓存。

练习记录表是整套报告系统的地基,字段是这样设计的:

CREATE TABLE practice_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, question_id TEXT NOT NULL, category TEXT NOT NULL, knowledge_point TEXT NOT NULL, is_correct INTEGER NOT NULL, selected_answer TEXT NOT NULL, duration_ms INTEGER NOT NULL, created_at INTEGER NOT NULL ); CREATE INDEX idx_records_created ON practice_records(created_at); CREATE INDEX idx_records_kp ON practice_records(knowledge_point);

每条记录必须冗余 category 和 knowledge_point,而不是通过 question_id 去题目表关联查。原因很实际:题目 JSON 是内存数据,如果报告聚合时要回表关联,既要跨存储又慢。冗余字段虽然在插入时多写几个字符串,但换来的是报告查询一次本地库就全部拿到。

错题归集我用了单独一张错题表,question_id 做唯一约束,答错自动插入,答对且已在错题表里的记录删掉。这样“错题清零”功能也顺手实现了。

3.3 模拟考试组卷逻辑

模考不是随机抽题那么简单,得按考纲比例来。我用的组卷算法是:按 category 的权重分层抽样,再做全局洗牌,最后用固定随机种子保证同一套卷子在不同设备上结果一致。

List<Question> buildPaper(List<Question> pool, Map<String, double> blueprint, int seed) { final random = Random(seed); final result = <Question>[]; blueprint.forEach((category, weight) { final bucket = pool.where((q) => q.category == category).toList()..shuffle(random); final need = (weight * totalCount).round(); result.addAll(bucket.take(need)); }); result.shuffle(random); return result; }

交卷逻辑有个坑:未做答的题在评分时不能漏掉,要按错误处理。模考记录也会写入 practice_records,但单独加一个 source 标识字段区分练习和模考,报告里这两个数据要分开看,混在一起会严重污染知识点正确率。

4. 学习报告实现:从埋点到图表

4.1 数据采集:一次答题记录什么

学习报告能不能做出来,全看埋点定义。我埋的字段就是 3.2 节那张表:question_id、category、knowledge_point、is_correct、duration_ms、created_at。这里要说三个容易漏的点:

  • duration_ms 必须逐题记。有了它才能算“平均每题用时”“知识点耗时”,这些指标对机构判断学生熟练度很有价值。
  • created_at 用毫秒时间戳,而不是存成“2025-04-01 10:00”这种格式化字符串。日期聚合只有在查询时才做格式化,存时间戳能避免时区问题和格式解析开销。
  • 会话时长单独处理。单题时长的总和并不等于用户实际学习时长,因为用户会在两道题之间发呆。我在刷题会话开始和结束时各记一个时间点,App 切后台时用 AppLifecycleState 回调自动结算本段,这个口径更接近真实学习投入。

4.2 统计口径:正确率、学习时长、连续天数怎么算

统计口径是最容易被产品和技术拉扯的地方,我踩过之后强烈建议:先写一份统计口径文档,再写代码。我最终落地的口径是这样的:

  • 正确率= 正确题数 ÷ 总题数。但模考中未作答的题一律计错,练习中用户中途放弃的不计。
  • 知识点掌握度= 该知识点答对次数 ÷ 该知识点练习次数。练习题量少于 3 条的知识点不展示,避免“练了一题对了就说掌握度 100%”的假象。
  • 学习时长= 会话时长累加,不是单题时长累加。
  • 连续学习天数= 从今天开始往前扫,只要当天练习记录数大于 0 就算连续,如果今天还没有记录,则从昨天开始扫。这样不会因为“今天还没开始学”就粗暴断掉连续记录。

聚合查询用 SQL 做也行,用 Dart 内存聚合也行。我这边因为已经全量查了 records,直接在内存里分组更顺手:

Map<String, double> knowledgeAccuracy(List<PracticeRecord> records) { final stat = <String, List<int>>{}; for (final r in records) { final s = stat.putIfAbsent(r.knowledgePoint, () => [0, 0]); s[1]++; if (r.isCorrect == 1) s[0]++; } return stat.map((kp, s) => MapEntry(kp, s[0] / s[1])); }

4.3 图表落地:fl_chart 折线与雷达图

学习报告页我做了四个可视化块:7日正确率趋势(折线图)、知识点掌握度(雷达图)、薄弱知识点 Top5(列表)、每日刷题量柱状图。

图表库选了 fl_chart,它在 OpenHarmony 上的表现让我意外地放心,因为它是纯 Dart 绘制,不依赖原生画布能力。折线图用 LineChart,雷达图用 RadarChart:

RadarChart( RadarChartData( dataSets: [ RadarDataSet( dataEntries: entries, borderColor: Colors.blue, fillColor: Colors.blue.withOpacity(0.2), ), ], radarShape: RadarShape.polygon, ticksTextStyle: const TextStyle(fontSize: 10), ), )

这里有个真实的坑要讲:RadarChart 在部分 OpenHarmony 设备上对触摸事件处理有兼容问题,如果用户手指滑过雷达图偶尔会闪退。我没花时间深究底层,直接把整个报告的图表区域包了一层 IgnorePointer,用户只能看不能点。报告页本来就是展示型页面,交互需求为零,这个取舍非常划算。

4.4 报告缓存与刷新策略

报告页打开时如果每次都全量扫 practice_records,数据量上来后会卡。我加了一张冗余聚合表 daily_stats:

CREATE TABLE daily_stats ( date TEXT PRIMARY KEY, total_count INTEGER DEFAULT 0, correct_count INTEGER DEFAULT 0, duration_ms INTEGER DEFAULT 0 );

每次答题完成,就在事务里更新当天这一行。打开报告页时,只需要查:daily_stats 算今日数据和 7 日趋势,practice_records 按 knowledge_point 分组算掌握度。两次小查询,毫秒级响应。

如果用户通过某些后台手段清了练习记录,daily_stats 会和 records 不一致。我在设置页放了一个“重建统计数据”入口,提供全量重建方法,报给用户“修复中”,省了很多客诉。

5. 跨端实坑清单:OpenHarmony 上插件与原生协作

5.1 插件兼容地图

这个项目做完,我对 Flutter 插件在 OpenHarmony 上的兼容性总结了一张表,给同行做个参考:

插件/库是否纯 DartOpenHarmony 可用性我的处理
provider是正常直接用
fl_chart是正常直接用,关闭触摸
shared_preferences否有社区适配版,版本需匹配找个固定版本锁死
path_provider否没有可用的自写 MethodChannel
sqflite否不可用改用 JSON + 文件存储

注意,插件兼容版本是动态的,今天能用的明天可能因为 SDK 升级挂掉。我的建议是:锁定 flutter_flutter 分支的 commit,锁定所有依赖的精确版本,绝不轻易升级。这个项目从联调到交付,工具链一个没升,就是因为稳定压倒一切。

5.2 自写 MethodChannel 对接 ArkTS

path_provider 用不了之后,我写了一个极简的 MethodChannel 拿应用文件目录。Dart 侧代码:

static const MethodChannel _channel = MethodChannel('app/native'); Future<String> getAppDir() async { return await _channel.invokeMethod('getAppDir'); }

原生侧是用 ArkTS 实现的,和 Android 上用 Kotlin 写插件是两套完全不同的 API。我这边只是在壳工程的 ArkTS 入口里注册了 channel 实现,返回文件系统路径。刚开始不熟悉 ArkTS 的模块注册规则,折腾了一天才跑通,原因只是我在错误的生命周期节点执行了注册。后来我把这一小段原生代码写成注释文档放在工程里,交给团队其他人就没再出过问题。

方法论上我想多说一句:自写 MethodChannel 的数量要克制。每多一个 channel,就多一个需要在原生侧验证的边界。这个项目把所有原生依赖收敛到了三个 channel:应用目录获取、轻量 KV 存储、设备信息。剩下的全靠纯 Dart 解决。

5.3 包体积和性能调优

HAP 发布包体积一开始很大,主要成本在 Flutter 引擎的 so 文件。我做三件事把它压了下来:

  • 发布构建加--obfuscate --split-debug-info,混淆的同时剥离符号表,体积减小明显。
  • assets 里只打包当前大纲对应考点的题目 JSON,不把全量题库历史版本打进去。
  • 题目配图统一压缩到 WebP,单张控制在 100KB 以内。

性能上最大的体感差异是 Debug 模式明显比 Android 卡,上 Release 包之后流畅度恢复正常。题库列表页我用了 itemExtent 固定行高,避免长列表滚动时的反复测量开销。这些在 Android 上可能无感的优化,在 OpenHarmony 的 Flutter 适配层上就是实打实的流畅度差距。

6. 交付之后回头看:这个项目真正的难点不在代码

6.1 时间花在了三个没写代码的地方

回头看整个交付,最花时间的三件事都不是 Flutter 代码本身:第一是题目 Excel 清洗成标准 JSON,几百道题的人工核对;第二是学习报告统计口径的确认,产品和教务来回拉扯了好几轮;第三是插件兼容性验证,每个依赖都要在真机上试一遍。

我自己的感受是:Flutter for OpenHarmony 的 UI 层开发体验已经相当够用,真正的不确定性全在生态层。哪个插件没适配、哪个原生 API 换了签名,这些问题不会出现在报错信息里,而是冷不丁在某个流程上绊你一下。

6.2 给后来者的三个选型标准

如果你也想入坑 Flutter for OpenHarmony,我建议你选依赖时用三条标准过滤:

  • 纯 Dart 优先,能查到官方维护的 OHOS 适配记录才考虑原生插件。
  • API 简单且可替换,万一插件挂了,自己写 MethodChannel 的成本要可控。
  • 版本锁死,不要追新。这套组合下,新版本带来的不确定远大于收益。

6.3 最后一点体会

做完这套题库,如果让我重来一次,我会在第一天就把“学习报告口径文档”和“依赖兼容矩阵”两个东西立起来,而不是边开发边补。艺考题库本身不难,难的是它要求你在一个适配还不成熟的平台上,把数据、统计、可视化完整跑通。但反过来说,正因为平台生态不成熟,先趟过这条路的人,后面再遇到同类的项目,基本就是降维打击了。希望这篇分享能帮你少踩几个我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询