说实话,第一次看到“Flutter for OpenHarmony”这个组合时,我第一反应是:这玩意儿能跑吗?毕竟OpenHarmony的底层接口和Android/iOS差了不少,Flutter的渲染引擎能在上面正常工作吗?带着这个疑问,我花了大概两周时间,从环境搭建到把一套完整的艺考真题题库app跑起来,中间踩了无数坑,也验证了一件事:Flutter确实能在OpenHarmony上做正经项目,而且做得还挺顺。
这篇博文不聊虚的,直接从我接手的这个“艺考真题题库+模拟考试”项目出发,讲讲我是怎么用Flutter把题库刷题、随机组卷、限时模拟考试这些功能在OpenHarmony设备上落地的。适合三种人看:准备在OpenHarmony设备上做应用的Flutter开发者、教育类app的产品或开发同学、以及想评估Flutter跨端价值的技术负责人。
1. 为什么选Flutter:题库类应用在OpenHarmony上的跨端最优解
1.1 先聊聊这个项目的背景
这个项目是给艺考生做的一套真题练习系统,覆盖美术、音乐、舞蹈、播音主持等专业的历年真题。核心诉求很朴素:学生能随时随地刷题、做模拟考、看错题解析。但有一个特殊点——学校那边有两类设备,一类是常见的Android平板,另一类是预装OpenHarmony的国产学习终端。如果两套都写原生,人力成本扛不住,产品迭代节奏也会被拖垮。
当时摆在桌上的方案有三个:ArkTS原生开发、Flutter for OpenHarmony、以及H5套壳。H5直接被否了,交互流畅度和离线刷题体验撑不住。剩下的时间成本,就是原生ArkTS和Flutter之间的取舍。
1.2 ArkTS原生和Flutter的真实差距
ArkTS是OpenHarmony上的一等公民语言,系统级API、设备能力调用都是原生最顺畅。但问题是:我们团队三个客户端开发全是Flutter背景,没人写过ArkTS。如果转过去,从语言、生态、组件库到状态管理全要重新学,保守估计要三周才能进入高效产出状态。
Flutter for OpenHarmony这边,项目本身是OpenHarmony SIG组在维护的社区方向,让Flutter引擎对接OpenHarmony的图形栈和系统服务。用得上的核心能力是:
- 同一套Dart代码可以编译成OpenHarmony的hap包,也能继续出Android/iOS包,后续如果客户又要Windows版本,一套代码接着走。
- Flutter自绘UI引擎不走系统原生控件,所以OpenHarmony和Android的控件差异对业务层的污染很小。我们的页面写出来,跑到OpenHarmony设备上,和跑到Android平板上,UI一致性非常高。
- pub.dev生态里大部分纯Dart包可以直接用,比如http、provider、sqflite这些,做题库这种应用完全不缺轮子。
那ArkTS的优势在哪儿?主要是在系统深度调用上,比如直接拿摄像头做AI识别、深度调优系统级推送。但题库app用不到这些——我们需要的系统能力就是存储、网络、计时器,Flutter侧都有对应的插件化方案。
1.3 我的最终判断依据
做了一个简单的评估表:
| 维度 | Flutter for OpenHarmony | ArkTS原生开发 |
|---|---|---|
| 团队上手成本 | 低,Dart/Flutter技能直接复用 | 高,需学习ArkTS和声明式UI范式 |
| 一套代码多端 | 支持,后续可出Android/iOS包 | 不支持,仅OpenHarmony |
| UI一致性 | 自绘引擎,跨端一致性强 | 依赖系统组件,随系统版本变化 |
| 第三方库生态 | pub.dev海量Dart包 | 起步阶段,组件和SDK相对少 |
| 系统能力调用深度 | 中等,插件桥接可扩展 | 最强,一等公民 |
题库类应用是典型的UI密集型+业务逻辑轻量型产品,Flutter的舒适区恰好在这里。于是定了:主技术栈Flutter,OpenHarmony作为核心交付平台,同时保留Android产物能力。
2. 环境搭建实战:OpenHarmony SDK与Flutter的版本匹配艺术
这一章先给结论:Flutter for OpenHarmony的环境搭建,80%的坑都出在版本匹配上。不是Flutter官方版本和OpenHarmony官方版本能够直接对齐的,社区分支有自己的一套版本节奏。
2.1 需要准备的工具清单
完整的环境需要四样东西:
- OpenHarmony SDK:通过DevEco Studio安装,IDE本身基于IntelliJ,对Flutter开发者来说不陌生。
- Flutter SDK(OpenHarmony社区版):不能用flutter官方渠道下载的版本,需要从OpenHarmony SIG的代码仓库拉取特定分支。
- DevEco Studio:用于构建hap包和连接设备调试。
- 真机或模拟器:OpenHarmony的模拟器需要DevEco Studio里的Device Manager创建,支持Phone和Tablet两种规格。
安装过程里我踩的第一个坑,是Flutter SDK的channel切换。社区维护的分支叫openharmony-xxx,但如果你已经装了官方Flutter SDK,千万不要直接在这个目录里切分支。两边虽然都叫Flutter,但SDK内部的平台通道层改动很大,混用会直接导致编译报错。正确做法是单独clone一份独立的目录,放在flutter-openharmony这样的路径下,和官方Flutter完全隔离。
2.2 环境变量与开发配置
装完之后,环境变量要做区分。我习惯在~/.zshrc里单独配一组:
export OHOS_HOME=/path/to/ohos-sdk export FLUTTER_OHOS=/path/to/flutter-openharmony export PATH=$FLUTTER_OHOS/bin:$PATH这里要注意,如果同时装了官方的Flutter和OpenHarmony版Flutter,终端里跑flutter命令时,得确定走的是哪一份。我建议做项目时只把OpenHarmony版加到PATH里,避免flutter命令和dart命令版本不一致导致的诡异问题。
接下来用flutter doctor检查环境,如果一切正常应该能看到OpenHarmony相关的提示项支持。我没见过doctor的OpenHarmony选项是绿色的,最多是黄色警告,但只要没有红色Error,一般不影响使用。
2.3 新建工程与设备连接
新建工程不能用flutter create直接生成标准项目,因为OpenHarmony需要额外的工程结构。我用的方式是以社区模板为基础,拿到之后改包名和目录名。项目里会多出一个entry模块目录,这是OpenHarmony的应用入口,类似Android的app模块。
设备连接调试用的是hdc命令,OpenHarmony版的adb:
hdc list targets hdc shell bm install -p /path/to/your.hap开发节奏跟Android开发很接近:改代码、flutter build hap、装包、看日志。日志查看用hdc shell hilog,说实话没有Android的Logcat好用,但凑合能定位问题。
2.4 第一个坑:新建项目跑不起来的排查链路
这里分享一下我最开始遇到问题时的完整排查思路。装好环境后,从模板建了一个空项目,flutter run到OpenHarmony设备上。结果直接卡住,报错信息指向某几个so文件加载失败,看起来是native库的架构不匹配。
排查过程:
- 第一步确认设备CPU架构:OpenHarmony平板是ARM64,构建产物也应该是ARM64。
- 第二步看构建日志,确认flutter build hap过程使用的是哪个ABI。发现模板默认的构建配置里带了arm64-v8a,但实际产物打包时没有正确引用so文件路径。
- 第三步是查工程里
build-profile.json5的externalNativeOptions配置,发现abiFilters没写,导致so文件没打进去。
补上:
{ "externalNativeOptions": { "abiFilters": ["arm64-v8a"] } }重新构建,空项目终于跑通了。这个坑提醒我一件事:OpenHarmony工程的构建脚本对原生库的声明格外敏感,凡是涉及插件或者引擎so库的,一定要先确认abiFilters。
3. 题库数据层设计:模型定义、SQLite存储与离线包策略
跑通Demo之后,接下来是真正的业务。题库app的地基是数据层设计,这块如果做不好,后面刷题页、模拟考页面再漂亮都是空中楼阁。我花了不少心思在设计题目模型、存储方案和内容更新策略上。
3.1 题目模型怎么定义
一道艺考真题,需要包含的信息比普通选择题多不少。以美术鉴赏题为例,题干可能是一张作品图片,选项涉及流派、作者、年代,答案还附带解析和扩展阅读。设计模型时我按最小完整粒度来划分:
class Question { final String id; final String subject; // 科目: 美术、音乐、舞蹈、播音... final String category; // 题型: 单选、多选、判断、填空 final String year; // 真题年份 final String stem; // 题干 final String? imagePath; // 题干图片,本地assets路径或URL final List<String> options; // 选项列表 final List<int> answer; // 正确答案索引,多选题多个值 final String analysis; // 答案解析 final int difficulty; // 难度等级 1-5 }options用字符串数组,answer用索引数组,这样单选多选判断都能统一表示。判断题就两个选项,答案索引0或1,逻辑完全复用。
3.2 SQLite还是JSON文件:我选择了sqflite
题库数据的体量比较特殊:一个科目大概2000-3000道题,每道题带图、带解析,JSON文件整体加载少说也要几十MB,而且全部常驻内存不现实。一开始有人提方案说用JSON文件直接解析,简单粗暴,但问题在于:模拟考要随机抽题、错题要按知识点筛选、刷题要有进度记录,这些都是典型的数据库查询场景,用JSON文件做筛选只会写出一堆效率极低的遍历代码。
最后用的是sqflite插件,在OpenHarmony上这个插件有对应的适配实现。表结构按模型拆成两部分:题目主表(不含解析)和题目解析表(拆分出来是为了刷题时减少IO)。查询时只加载题目,点开解析时才去查解析内容。
建表SQL长这样:
CREATE TABLE questions ( id TEXT PRIMARY KEY, subject TEXT, category TEXT, year TEXT, stem TEXT, image_path TEXT, options TEXT, answer TEXT, difficulty INTEGER ); CREATE TABLE question_analysis ( id TEXT PRIMARY KEY, analysis TEXT );options和answer用JSON字符串存,Dart侧做序列化转换。这在sqlite里属于常规操作,查询性能完全够用。
3.3 离线包与增量更新的思路
题库app很特殊的一点是:用户在考场上可能根本没网。所以内容必须以离线包形式随app分发,服务器再定期增量更新。
我的方案是这样的:每个科目的题库打包成一个zip,放在assets目录里随hap包发布。首次启动时,app将zip解压到应用私有目录的数据库文件中。增量更新走服务器接口,服务器返回的就是SQL语句列表或JSON diff,客户端拉到后自动执行。
之所以不用整包更新,是因为艺考题库每年都会出新一年的真题,如果每次更新都重新下载几个GB的完整包,用户体感会很差。增量更新的处理逻辑要做幂等,防止重复更新导致数据重复。
增量更新的伪代码:
Future<void> applyRemoteUpdate(List<QuestionUpdate> updates) async { final db = await DatabaseHelper.instance.database; await db.transaction((txn) async { for (final update in updates) { if (update.type == 'insert') { await txn.insert('questions', update.question.toMap()); } else if (update.type == 'delete') { await txn.delete('questions', where: 'id = ?', whereArgs: [update.id]); } } }); }这套逻辑跑下来,新增一个科目也就几十秒的下载和几十毫秒的入库时间,在4G网络下体验还算可以。
4. 刷题页与答题卡:组件划分、provider状态管理与组件通信实战
框架跑通、数据层就绪后,重头戏是UI层。艺考题库的交互集中在两块:刷题模式和模拟考试模式。这两块在组件设计上有很大差别:刷题模式要支持上下题切换、收藏、笔记;模拟考试要严格限时、答题卡导航、自动交卷。但底层的题面展示组件是复用的。
4.1 题面组件的抽离
不管刷题还是模拟考,一道题的展示区基本是一样的:题干、图片、选项列表、答案解析。所以我抽了一个QuestionCard组件,接收Question对象和当前状态,内部渲染题干和选项区。选项的选中状态、正确的绿色高亮、错误的红色提示都在这个组件里完成。
组件通信上,QuestionCard不直接修改全局数据,而是通过回调把用户的选项点击事件抛给上层页面。这样设计的原因是:刷题模式和模拟考模式下,选项点击后的行为完全不同。刷题模式点击选项立刻显示对错并自动滚到下一题;模拟考模式点击选项只做标记,不能立刻看到答案。如果把这个逻辑放组件内部,组件就要感知业务模式,耦合度会变高。
class QuestionCard extends StatelessWidget { final Question question; final QuestionStatus status; final void Function(int index)? onOptionTap; @override Widget build(BuildContext context) { // 渲染题干和选项,点击时回调onOptionTap } }4.2 provider怎么用:状态管理选型与实操
页面拆完,接着要解决状态管理。项目规模不大,但状态分散:当前题目索引、已答记录、计时器剩余时间、答题卡选中状态、收藏状态、错题标记……如果用setState硬写,页面传参要传晕,状态一多没法维护。
Flutter生态里状态管理方案很多,bloc、riverpod、provider、getx各有拥趸。考虑到团队熟悉度和项目规模,我最后选了provider。核心逻辑很清晰:全局数据放在ChangeNotifier里,需要响应数据变化的地方用context.watch监听,不关心的组件不重建。
考试状态管理类大致长这样:
class ExamState extends ChangeNotifier { List<Question>? _examQuestions; Map<String, List<int>> _answers = {}; int _currentIndex = 0; int _remainingSeconds = 2700; Timer? _timer; List<Question>? get examQuestions => _examQuestions; Map<String, List<int>> get answers => _answers; int get currentIndex => _currentIndex; int get remainingSeconds => _remainingSeconds; void startExam(List<Question> questions) { _examQuestions = questions; _answers = {}; _currentIndex = 0; _remainingSeconds = questions.length * 90; // 每题90秒 _timer?.cancel(); _timer = Timer.periodic(Duration(seconds: 1), (timer) { _remainingSeconds--; if (_remainingSeconds <= 0) { timer.cancel(); autoSubmit(); } notifyListeners(); }); notifyListeners(); } void selectAnswer(int optionIndex) { _answers[_currentQuestion.id] = [optionIndex]; notifyListeners(); } void jumpTo(int index) { _currentIndex = index; notifyListeners(); } }这段代码做了几件事:模拟考开始时初始化题目列表、清空答案、设置总时间并按题型乘以不同的秒数(比如选择题90秒/题,主观题120秒/题)、启动一个每秒走一次的计时器、到点自动交卷。选答案和跳转题目的操作都会触发notifyListeners,所有监听这个状态的组件会同步刷新。
4.3 答题卡组件的状态同步
答题卡是整个app里组件通信最绕的部分。它不是一个普通静态网格,而是需要实时反映每道题的回答状态。用户从答题卡点击某题跳转过去,回来之后答题卡必须保持正确标记。
我的做法是让答题卡组件只负责渲染,数据全部从ExamState读取:
class AnswerSheet extends StatelessWidget { const AnswerSheet({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final examState = context.watch<ExamState>(); final questions = examState.examQuestions!; return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, ), itemCount: questions.length, itemBuilder: (context, index) { final question = questions[index]; final answered = examState.answers.containsKey(question.id); final isCurrent = index == examState.currentIndex; return GestureDetector( onTap: () => examState.jumpTo(index), child: Container( color: isCurrent ? Colors.blue : answered ? Colors.green.shade300 : Colors.grey.shade300, child: Center(child: Text('${index + 1}')), ), ); }, ); } }关键点在于:答题卡不是主动维护自己的UI状态,它只是ExamState的观察者。用户点了选项,_answers变化,notifyListeners触发,答题卡自动刷新。用户点答题卡上的题号,jumpTo改变_currentIndex,答题卡又自动高亮新题目。这样组件之间其实没有显式通信,都是通过共享状态自动达成一致。这也是provider这类响应式状态管理相比传统回调传参的最大优势。
4.4 刷题模式下的进度记录
刷题模式比模拟考简单些,不需要计时也不需要交卷,但需要持久化刷题进度。用户刷到第80题退出app,下次进来应该回到第80题。这个进度我用shared_preferences存,每次切换到新题目时更新:
class PracticeState extends ChangeNotifier { final Map<String, int> _progress = {}; Future<void> loadProgress() async { final prefs = await SharedPreferences.getInstance(); for (final subject in subjectList) { _progress[subject] = prefs.getInt('progress_$subject') ?? 0; } notifyListeners(); } Future<void> saveProgress(String subject, int index) async { _progress[subject] = index; final prefs = await SharedPreferences.getInstance(); await prefs.setInt('progress_$subject', index); } }这里有一个体验上的细节:保存进度不能每次翻题都同步等待写入完成,那样滑动会卡顿。做法是先更新内存数据、通知UI刷新,然后异步写入SharedPreferences。即使写入失败,下次启动时最多就是回到前几题,影响不大。
5. 模拟考试三件套:随机组卷、限时计时和交卷判分
模拟考试是本项目的功能高地,也是体现产品价值的一块。艺考模拟考不是简单的随机抽题,而是要尽可能还原真实考试规则:按题型结构出题、限时交卷、即时判分、查看解析。这一章把三个核心功能的实现拆开讲。
5.1 组卷算法:从题库里抽出一套符合标准的卷子
真实的艺考真题卷结构是有规律的。以美术为例:选择题20道、判断题10道、简答题3道、论述题1道。模拟考如果纯随机抽,很可能抽出来的卷子全是选择题,或者简答题难度过高。所以组卷要按题型和难度双重约束来设计。
我的组卷逻辑分三步:
- 从题库里按科目筛选出题目候选集。
- 按题型分组,每个题型内部按难度比例分层(难度1-2占30%,难度3占40%,难度4-5占30%)。
- 在每个分层里做随机抽取,抽够目标数量后合并,返回final的题目列表。
Dart代码实现:
List<Question> generateExamPaper({ required List<Question> bank, required String subject, required Map<String, int> structure, // { '单选': 20, '判断': 10, '简答': 3 } }) { final filtered = bank.where((q) => q.subject == subject).toList(); final result = <Question>[]; structure.forEach((category, count) { final categoryQuestions = filtered.where((q) => q.category == category).toList(); // 按难度分层 final easy = categoryQuestions.where((q) => q.difficulty <= 2).toList()..shuffle(); final medium = categoryQuestions.where((q) => q.difficulty == 3).toList()..shuffle(); final hard = categoryQuestions.where((q) => q.difficulty >= 4).toList()..shuffle(); final perTier = count ~/ 3; result.addAll(easy.take(perTier + (count % 3 > 0 ? 1 : 0))); result.addAll(medium.take(perTier)); result.addAll(hard.take(perTier + (count % 3 > 1 ? 1 : 0))); }); return result..shuffle(); // 最后整体打乱题型顺序 }这里有一个细节:..shuffle()是用Dart的级联操作符对列表原地打乱。组卷的随机源用的是默认的Random(),如果是正式发布的版本,建议传一个固定种子进去,这样同一套配置抽出的卷子是确定的,方便复现和测试。debug模式下固定种子排查问题也更容易。
5.2 计时器的跨页面生命周期管理
计时器是最容易写出bug的部分。一个典型的坑是:用户在考试页和答题卡页之间来回切换,如果不小心在每个页面都启动一个计时器,结果就是时间走得飞快,甚至出现负剩余时间。
我的处理原则是:计时器只能有一个,且它的生命周期必须绑定状态类,而不是绑定页面widget。ExamState里的Timer从考试开始启动,用完整个考试周期。页面切换只是改变显示方式,计时器不受影响。widget销毁时不需要取消计时器,因为状态对象还在,计时器继续跑。
但要注意一个问题:如果整个页面(比如整个考试包含Container和评价结果的页面)被pop出导航栈,ExamState变成无引用状态时,计时器仍然在跑,就会造成资源泄漏。解决方案是在dispose方法里标记考试结束并取消Timer:
@override void dispose() { _timer?.cancel(); super.dispose(); }Provider在页面被pop时会调用dispose,只要确保不再notifyListeners,就不会触发“大量widget在树外更新”的报错。
5.3 交卷判分:客观题全自动,主观题做标记
交卷后的判分逻辑,客观题(单选、判断、多选)在本地直接判分,主观题(简答、论述)不做内容识别,只是标记为“待人工评分”,并在成绩报告里显示未评分题目的数量。这个设计符合实际场景:艺考的主观题答案千变万化,系统能做关键词匹配但不靠谱,不如直接告诉学生“这道题写完对照解析自己评估”。
判分逻辑:
Map<String, dynamic> gradeExam(ExamState examState) { final questions = examState.examQuestions!; final answers = examState.answers; int correctCount = 0; int subjectiveCount = 0; final subjectiveIds = <String>[]; for (final q in questions) { final answered = answers[q.id] ?? []; if (q.category == '简答' || q.category == '论述') { subjectiveCount++; subjectiveIds.add(q.id); continue; } if (_isCorrect(q, answered)) { correctCount++; } } return { 'total': questions.length, 'correct': correctCount, 'subjectiveCount': subjectiveCount, 'subjectiveIds': subjectiveIds, 'score': (correctCount / (questions.length - subjectiveCount) * 100) .toStringAsFixed(1), }; }这里的得分计算逻辑是:客观题部分,每题1分折算成百分制;主观题不计入得分但会在报告中展示。艺考学生关注的往往是客观题的正确率,主观题靠自我对照,这套逻辑符合用户预期。
5.4 成绩报告的结构设计
交卷后展示的成绩报告,至少要包含:总分、客观题得分率、各题型正确率、错题解析列表。这里用到前面判分的返回结果,再组装一个报告页面。错题解析列表直接取ExamState里所有答错的题目ID,然后从题库里查出完整的题目和解析内容。
有一点要提醒:成绩报告生成后,需要把本次考试记录存入本地数据库,方便用户随时查看历史成绩和进步曲线。我建了一张exam_records表,字段包括日期、科目、总题数、正确数、得分。历史记录页面按日期倒序展示。
6. 真机适配与HAP打包:上线前踩过的RenderFlutter和内存坑
最后一个环节是把app真正装到OpenHarmony设备上,这一步涉及的坑比开发阶段还多。开发阶段在平板上跑得很顺畅,真机上一跑就是各种渲染问题。
6.1 题面图片的加载与内存控制
艺考题库大量题目带图,尤其是美术作品图的解析图。App里用的图片加载方案是cached_network_image,但OpenHarmony环境对原生网络栈的适配有差异,部分旧版本cached_network_image在OpenHarmony上有加载失败的问题。我的规避方案是:所有图片资源优先走本地assets离线包,服务器更新的图片先下载到本地再展示,不走网络懒加载。离线包的图片按题目ID命名,查询时直接映射到本地路径。
真机上内存吃紧的问题也出现过,主要是加载大图时Flutter引擎的图片缓存没有及时释放。解决方式是使用image_cache的清理策略:每20张题目图片设置一次缓存限制,超过就调PaintingBinding.instance.imageCache.clearLCU()清理低频使用的图片缓存。实测内存峰值从480MB降到290MB,效果明显。
6.2 不同分辨率与字体缩放的适配
OpenHarmony的学习终端分辨率跨度很大,有1280x800的小平板,也有2560x1600的高分平板。我的处理方式是:页面整体用MediaQuery.of(context).size获取宽度,按宽度设计一套动态缩放比例,不写死px。
double rs(double size, BuildContext context) { final width = MediaQuery.of(context).size.width; return size * (width / 1280); // 以1280宽度为基准 }字体大小全部用rs函数计算,控件间距同理。这套方案在Android上当然是老技术了,但OpenHarmony平台的适配我实际测下来,同样的逻辑完全适用。唯一需要注意的坑是:OpenHarmony的系统字体设置可能会影响Text组件的渲染,应用内最好统一指定字体族和fontSize,避免跟随系统字体大小变化导致布局错乱。
6.3 构建HAP包的正确姿势
打包阶段我也踩了一个大坑:直接用flutter build apk的习惯在OpenHarmony上不适用,需要先确认flutter命令是否指向OpenHarmony分支,然后执行:
flutter build hap --debug生成的hap包位置在build/ohos/outputs/hap/debug/,用hdc命令即可安装:
hdc install build/ohos/outputs/hap/debug/your_app.hap这里最需要注意的是Debug和Release的区分。Debug包用于开发调试,但性能较差;Release包需要额外做签名和混淆配置。我们项目在上测试机时发现Debug包页面切换有卡顿,还以为是Flutter引擎性能问题,后来换成Release包就流畅了。这提醒我:调试阶段用Debug看日志,性能验证必须用Release包,这是OpenHarmony开发中一定要记住的规则。
6.4 与OpenHarmony原生能力的桥接:目录访问与文件读写
最后聊一下与原生能力的桥接。题库更新需要下载zip包然后解压,这涉及文件系统访问。Flutter的标准库dart:io在OpenHarmony上是可用的,但有一些目录访问差异。OpenHarmony应用有自己的沙箱目录,直接读取外部存储路径会失败。我的解决方案是通过path_provider插件获取应用私有目录,所有文件操作都在私有目录下进行。真机上测试,下载、解压、存取数据库都没问题。
遇到一个比较特殊的问题:path_provider在OpenHarmony上拿到的外部存储目录和Android拿到的不一致,代码里要根据平台区分。实现上用一个工具类做封装,dart侧判断Platform.isAndroid还是自定义的OpenHarmony判断。由于Flutter官网没有暴露OpenHarmony的platform标识,我用的终极方案是判断defaultTargetPlatform不匹配时,降级使用getApplicationSupportDirectory,虽然只访问私有目录,但对于题库app完全够用。
找一段我的实践切身体会
从环境搭建到模拟考试全部跑通,我用了一周半,中间有两件事印象最深。
第一次在OpenHarmony真机上通过flutter run看到熟悉的Flutter渲染画面时,有点恍惚——过去只能在Android/iOS上跑的Flutter应用,居然真的在一个国产开源操作系统上跑起来了。而且跑的不是Demo,是一套完整的题库产品,包括SQLite存储、随机组卷、倒计时交卷判分,这些逻辑全部复用原有Android版本的那套Dart代码,改动量很小。
第二件事是模拟考试功能联调时,真机上的计时器在校验阶段出现了10秒左右的偏差。排查后发现是Flutter Timer在OpenHarmony上受系统电源管理影响,设备息屏后定时器被降频。解决方案是在考试期间用WakeLock申请保持唤醒状态,保证计时器稳定运行。这个坑在Android平台上我就听说过,但是没想到OpenHarmony上也会存在,以后所有涉及长时间计时的功能都要提前做防息屏处理。
最后再说一个有用的习惯:开发过程中我每周都把整套OpenHarmony构建环境跑一遍CI,确保环境变化不会在关键节点上掉链子。OpenHarmony的SDK、Flutter分支、插件适配一直在活跃迭代,三个月不更新就可能有兼容性问题。这个项目用完,我最大的体会是:Flutter的跨端优势在OpenHarmony上不是理论上的,而是实打实的生产力工具。如果你手上也有一个需要在多端复用的业务型App,认真评估一下Flutter for OpenHarmony,值得的。