news 2026/9/11 23:05:32

HarmonyOS 4刷题APP开发:Stage模型、ArkUI与RDB持久化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS 4刷题APP开发:Stage模型、ArkUI与RDB持久化实践

简介:本代码包是一款基于HarmonyOS 4开发的刷题型鸿蒙应用完整工程,面向正在学习鸿蒙开发或需要完成毕业设计、期末大作业的开发者。项目围绕HarmonyOS基础架构、分布式任务调度、UI框架与组件、DevEco Studio工程配置等核心知识展开,通过真实刷题场景演示了从题目列表、答题页到结果统计的完整交互流程,并接入云存储模块,适合对照源码理解HarmonyOS API调用与跨设备数据管理思路。包内共1793个文件,约5.35MB;主体为js/ts/ets源码文件、pem证书、json/json5配置文件,以及png/svg图标和md说明文档,目录结构清晰,包含答题页、结果页、云存储等模块,便于按功能快速查找。已有611人浏览学习。借助该工程可快速复现刷题应用的数据存储、题目展示与答题交互逻辑,是快速上手鸿蒙4开发、课设与项目答辩的实用参考。

1. 刷题APP是HarmonyOS 4最能“所见即所得”的入门实践

如果把一份基于 HarmonyOS 4 开发的刷题APP源代码压缩包拿到手里,第一反应别是去找“运行说明.txt”,而是先把它当作一个普通Stage工程来拆。刷题类应用的典型路径是:题库从本地JSON或数据库读出,页面在列表和答题之间切换,选项点击后判分并记录错题。这个骨架几乎覆盖了鸿蒙原生开发里最常考的四个能力:ArkTS类型系统、ArkUI响应式渲染、关系型数据库持久化、页面路由。这个所谓“高分项目”通常不是靠算法,而是靠工程完成度拿分。下面要做的是把这条路径重走一遍,让拿到源码的从业者或正在备考HarmonyOS应用基础认证的开发者,能在真机上看到一个可运行的刷题闭环,而不是停在解压页面。

2. HarmonyOS 4刷题APP的工程骨架与Stage模型选型

一份能上真机跑的鸿蒙刷题APP源代码,工程结构基本都围绕Stage模型展开。可能有些从2023年之前模板生成的老代码还在用FA模型,那时页面和Ability绑定得很紧,页面跳转像是切进程。HarmonyOS 4的DevEco Studio新建项目已经默认推荐Stage模型,刷题APP选它不只是为了“新”,更重要的是它能让一个应用只有一个UIAbility,题库列表、答题页、错题本这些页面都在同一个窗口内用路由切换,答题做到一半去查知识点,再回来时当前进度不会被系统当作后台任务回收。

2.1 为什么刷题APP优先选Stage模型而不是FA模型

FA模型里一个页面可以对应一个Ability,切换页面等于切换Ability,页面之间的全局状态很难维护;Stage模型统一由UIAbility承载窗口,页面路由通过main_pages.json和路由API完成。刷题APP里的“章节练习”“答题页”“错题本”只是同一个UIAbility内的不同页面,这样状态不会被系统频繁回收。

从开发体验看,Stage模型还提供了AppStoragePersistentStorage,刷题进度这种全局数据可以直接挂到全局存储上,不用自己写单例工具类。这给答题页和结果页的数据共享省了很多事。对答辩更友好的一个理由:FA模型在系统资源紧张时Ability可能会被回收,而Stage模型的应用重启后可以通过SavedState恢复页面,刷题做到一半切走再回来,当前题目还能留住。

这里也有一个容易误判的点:有人看到源代码里没有“第二入口”就怀疑不是Stage模型。实际上Stage模型不一定只有EntryAbility,刷题项目一般一个UIAbility就够。真正要判断模型的方法是看有没有module.json5,以及abilities数组里的srcEntry指向是不是ets/entryability/EntryAbility.ts

2.2 读懂module.json5与main_pages.json这两个入口配置

拿到源代码以后,我一般先看三个文件:AppScope/app.json5entry/src/main/module.json5entry/src/main/resources/base/profile/main_pages.json。在源码包被解压、导入IDE之前,先确认这三个文件是否存在,能帮你判断这个项目是完整工程还是只有部分源码。

module.json5里的abilities数组负责声明Ability,但页面列表不在这里,而是在main_pages.json。刷题APP的main_pages.json通常长这样:

{ "src": [ "pages/Index", "pages/QuestionBank", "pages/ExamPage", "pages/ResultPage" ] }

这里src数组的顺序决定的是页面路由能否直接通过url访问,渲染时并不一定按数组顺序展示。刷题APP一般把Index作为首页,把ExamPage注册成答题页,这样router.pushUrl({ url: 'pages/ExamPage' })才能找到对应页面。常有人把main_pages.json删了导致路由跳转报错page not found,这个文件不是装饰,是路由表。

2.3 用DevEco Studio导入“高分项目”zip的最小步骤

提示:如果是别人打包的zip,先确认压缩包第一层是AppScope还是entry。如果是后者,解压后把外层文件夹指给DevEco即可,不要新建工程再拷贝源码。

导入步骤可以压缩成三行:

  1. 打开DevEco Studio,选择File > Open,选择解压后的工程目录,而不是zip本身。
  2. 等待ohpm install自动同步依赖;如果没有自动同步,在菜单Tools > Ohpm里执行ohpm install
  3. 打开entry/src/main/ets/pages/Index.ets,先点Previewer看首页渲染,再连真机或模拟器运行。

如果工程是从旧版本SDK创建,导入后会提示升级hvigor或SDK,这里建议点“稍后”而不是直接升级,因为升级hvigor经常把build-profile.json5里的配置一起改了,导致一堆签名错误。等到能跑通一遍真机,再考虑版本升级。

整个Stage模型对刷题这种单一入口应用来说其实有点“重”,但它提供的AppStoragePersistentStorage能省去自己造全局变量的麻烦。这里的选型逻辑可以用一个表讲清楚:

维度FA模型Stage模型
页面跳转Ability之间绑定地址路由或Navigation
全局状态靠DataAbility或外部存储AppStorage/Environment
本地数据文件+偏好RDB/Preferences
代码组织页面与Ability耦合分层清晰,易改

3. 题库数据建模与本地持久化:从JSON到关系型数据库

刷题APP的源代码里,最值得自己改的部分是题库数据。很多“高分项目”把题库做成几千行JSON塞在rawfile里,运行时不切实际;另一种做法是首次启动时把JSON导入RDB,之后所有查询都走SQL。这个设计决定了答题体验:5000道题的库如果每次全部装入内存,翻页时肉眼可见卡顿,而RDB查询只在当前章节取50道题,内存占用会稳定很多。

3.1 用TypeScript定义刷题数据模型

ets文件里可以直接使用TypeScript接口,一个选择题最少要有题目、四个选项、正确答案、章节标签和解析。下面是我常用的模型:

export interface Question { id: number; category: string; type: 'single' | 'multiple' | 'judge'; content: string; options: string[]; answer: number[]; // 单选时只有1个元素,多选是多个下标 explanation: string; difficult: 1 | 2 | 3; // 1简单,2中等,3难 }

answernumber[]而不是number,是因为判断题和单选题可以统一成“下标数组”,避免为单选、多选各写一套判分逻辑。type字段用字符串而不是布尔值,是为了后续加入“看题模式”和“考试模式”时能直接判断题型。解析接口字段用explanation,很多项目写成analysis,虽然不影响运行,但会给错题本页面读取增加一次字段映射,建议统一命名。

3.2 RDB建表与索引设计

使用鸿蒙的关系型数据库,需要先获取RdbStore然后建表。以一次建两张表为例:

CREATE TABLE IF NOT EXISTS question ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, options TEXT NOT NULL, answer TEXT NOT NULL, explanation TEXT, difficult INTEGER DEFAULT 1 ); CREATE INDEX idx_question_category ON question(category); CREATE INDEX idx_question_type ON question(type);

optionsanswerTEXT保存,其中options存JSON字符串,answer[1]这样的字符串。或者使用RDB结果转换时再展开。这里需要说明:刷题APP每次答题只需要当前章节数据,category上的索引会让where category = ?非常快;不要为content加索引,因为长文本索引既占空间又对like查询没有明显收益。

EntryAbilityonWindowStageCreate前后初始化RdbStore,用RdbPredicates查询。代码较长,但核心流程是这样的:

import { relationalStore } from '@kit.ArkData'; const config: relationalStore.StoreConfig = { name: 'quiz.db', securityLevel: relationalStore.SecurityLevel.S1 }; relationalStore.getRdbStore(this.context, config, (err, store) => { store.executeSql('CREATE TABLE IF NOT EXISTS question (...)'); });

securityLevel是HarmonyOS必须设的一个参数,刷题这种没有通信敏感数据的应用用S1即可,设成S3会导致加密逻辑更慢,真机调试时每次冷启动变长。StoreConfig里还能指定encrypt,但普通刷题项目不必开启。

3.3 第一次启动时把rawfile里的JSON导入RDB

“高分项目”里通常带一个resources/rawfile/questions.json。要把它变成可查询的数据,需要在首次启动时读取并循环插入。这里不建议在主线程一条条insert,5千条数据会让白色启动页多停留2秒以上。常见做法是:

  1. @ohos.resourceManagergetRawFileContent读取questions.json
  2. 在一个新创建的RdbStore事务里批量插入。
  3. Preferences写一个db_initialized标记,避免下次启动重复导入。

导入完成后,错题本、章节练习、随机刷题三个页面都从这张question表取数。这样代码的可维护性比“直接改JSON”高很多。真正的坑在于:getRawFileContent拿到的是Uint8Array,需要先转成字符串,再用JSON.parse。很多源代码项目在这里直接把二进制对象当字符串用,导致运行时不报错但题库是空的。

关键参数说明:executeSql方法只接受SQL文本,不参与参数绑定;需要动态参数时用store.insertRdbPredicates。表结构变更时,需要先执行ALTER TABLE,或者卸载应用重新跑,这个在开发阶段可以省事但交付前不要这样。

4. 用ArkUI实现刷题页面与答题交互

刷题APP在UI层面的核心是答题页。一个答题页要处理三件事:题目展示、点击选项、判分后跳到下一题。HarmonyOS 4的ArkUI用@Component@State实现响应式界面,代码模式可以统一成:一个@State保存当前题目索引,一个@State保存用户选择,一个计算属性返回是否正确。这个页面的状态清晰了,后面的错题本才能拿到需要的数据。

4.1 从题库列表页路由到答题页并携带参数

router.pushUrl传参时,参数长度有限制,一般只传题库ID和章节名,不要传整个题目对象。示例:

import { router } from '@kit.ArkUI'; router.pushUrl({ url: 'pages/ExamPage', params: { category: this.currentCategory, startIndex: this.startIndex } }).then(() => { console.info('push exam page success'); }).catch((err: Error) => { console.error(`push failed, code is ${err.code}, message is ${err.message}`); });

这里的params会在ExamPageonPageShowaboutToAppear里用router.getParams()接收。接收后要立刻深拷贝一份,因为router.getParams()拿到的对象与源页面可能共享引用,在ArkUI里页面被回收时参数引用行为会变,建议用结构化克隆。

4.2 用@State和ForEach渲染选项与选中反馈

答题页的@State selected: number[]用来记录本次选择,@State current: number记录当前是第几题。页面主体用一个Column包含题干和选项:

@Builder OptionButton(option: string, index: number) { Button(option) .width('100%') .backgroundColor(this.selected.includes(index) ? '#0A59F7' : '#FFFFFF') .fontColor(this.selected.includes(index) ? '#FFFFFF' : '#182431') .onClick(() => this.toggleSelect(index)); }

整个列表用ForEach(this.currentQuestion.options, (option: string, index: number) => ...)渲染。注意ForEach的第二个参数会给到两个参数,第一个是元素,第二个是索引,不要反过来写。这也是很多源代码项目运行后点击任一项都选中第一项的原因。

toggleSelect里要区分单选和多选:

toggleSelect(index: number) { if (this.currentQuestion.type === 'single' || this.currentQuestion.type === 'judge') { this.selected = [index]; } else { const pos = this.selected.indexOf(index); if (pos > -1) { this.selected.splice(pos, 1); } else { this.selected = [...this.selected, index]; } } }

这里splice直接改数组是能触发界面刷新的,因为this.selected是被@State观察的引用,重新赋值会整体刷新;但为了保险,多选时我用展开运算符生成新数组再赋值,避免数组内部修改后状态检测不到的问题。这个机制在HarmonyOS 4的@State里其实通过代理已经处理,但写成不可变更新风格能少踩一半坑。

4.3 倒计时与自动交卷的实现

考试模式需要一个倒计时,用setIntervalaboutToAppear里启动、在aboutToDisappear里清掉:

this.timerId = setInterval(() => { this.remainSeconds--; if (this.remainSeconds <= 0) { clearInterval(this.timerId); this.submitPaper(); } }, 1000);

remainSeconds@State,每秒自减一次,界面上的进度条会自动推进。这里有一个很重要的参数:setInterval返回的类型在TS里是number,在ArkTS里建议定义成number类型,不要用NodeJS.TimerReturnType<typeof setInterval>,因为ETS编译器对后者的类型定义经常报错。submitPaper()里要同时处理三件事:计算得分、写入答题记录、跳转结果页。得分计算不要循环两次,可以在用户选择题目的同时就维护一个Record<number, number[]>,交卷时只需要和答案做一次对比。

状态变量作用刷新时机
@State current当前题目在列表中的索引点击“下一题”后自增
@State selected当前题目的选中项列表点击选项时重新赋值
@State remainSeconds剩余秒数setInterval每秒触发
@Prop category父组件传入门类路由参数赋值时

这张表对应的是答题页最常见的四个状态。需要强调,不要把题目列表整个放进@State,因为两千道题的数组一旦变化,Diff更新的成本会上升,答题时只保留当前题号和用户作答映射,列表交给上一层的“章节练习”去管理。这是刷题APP性能优化的一个简单分层。

5. 错题本、知识点标签与刷题记录的进阶实现

基础答题流程跑通后,源代码项目的“高分感”体现在错题本和进度记录上。许多刷题APP源码做到了“答完题弹个分数”就结束,而一个完整的学习闭环应该能让用户随时回看错题。错题本的核心是把答题结果持久化,并且保证只在“答错”时写入,而不是在选项点击时写入。

5.1 错题入库的时机与幂等设计

错题本表可以设计成独立表,不污染题库表:

CREATE TABLE IF NOT EXISTS wrong_book ( question_id INTEGER PRIMARY KEY, wrong_count INTEGER DEFAULT 1, last_answer TEXT, updated_at INTEGER );

这里用question_id作为主键,就可以用insert or replace或者先查再更新的方式来防止一条错题重复插多条。我一般用store.insert前先查一次,命中则update;如果数据量很大,可以直接使用INSERT INTO wrong_book(question_id, wrong_count) VALUES(?, 1) ON CONFLICT(question_id) DO UPDATE SET wrong_count = wrong_count + 1,这条SQL在HarmonyOS的RDB里也能跑,前提是把表主键建好。

在判分之后,用当前题目的id去判断是否与标准答案数组一致,不一致再写错题。千万不要在读题页面点一下选项就写库,那样用户只是看了一眼,还没确定,错题本就不干净了。

5.2 用AppStorage同步答题进度与全局状态

答题总进度、章节通过率需要跨页面共享。HarmonyOS 4提供了AppStorage,可以在首页、答题页、错题本之间共享同一个键值。最简单的做法是在EntryAbilityonCreate里先持久化恢复,再写入内存:

AppStorage.setOrCreate('totalAnswered', 0); AppStorage.setOrCreate('wrongCount', 0);

页面里读取时:

@StorageProp('totalAnswered') totalAnswered: number = 0; @StorageLink('wrongCount') wrongCount: number = 0;

它们的区别在于@StorageProp是本页单向同步,别的页面修改后当前页自动变,但当前页不能反向改;@StorageLink是双向同步,改当前页也会写回AppStorage。如果只是为了“统计答了多少题”,用StorageProp即可,避免答题页不小心把全局进度清零。

需要特别说明的是:AppStorage只在应用运行期间存活,应用杀进程后会丢失,所以要在合适的生命周期把totalAnswered写回Preferences。错题本表已经落库,进度可以用getPreferences保存,不要用@StorageLink和持久化混淆。

5.3 真机调试时“当前不会命中断点”的常见原因

很多人在DevEco Studio里给Index.ets打一个断点,运行后提示“当前不会命中断点”。这个问题在HarmonyOS 4开发中大概率不是代码逻辑问题,而是断点打在了编译产物与源码映射不一致的地方。以下几种情况可以逐一排查:

  1. 代码路径里有中文或空格,DevEco的sourcemap在某些版本里会错位,尽量把工程路径改成纯英文。
  2. 断点打在@Builder方法内部渲染的子组件上,构建过程会重新排列代码,断点位置可能漂移。
  3. 开启了release编译运行,release包默认不携带调试信息。在真机调试时选择debug变体,同时把hvigor的构建模式设为debug

还有一个和源码包相关的问题:从zip解压出来的源代码里可能有多个entry目录或残留的.hvigor缓存。导入前把.hvigoroh_modulesbuild三个目录删掉再重新同步,能解决很多“改代码不生效”和“断点不命中”的问题。因为缓存中的编译产物还是旧的。

5.4 从“高分项目”源代码里迁移出自己题库的方法

拿到别人的刷题APP源代码,最终要改成自己的题库。常见做法是:用Python脚本读取Excel或CSV,json.dumpquestions.json,再替换rawfile里的同名文件。注意编码要UTF-8,不能带BOM;另外,JSON里的每个字段名称要和Question接口完全对应。如果字段对不上,导入时大概率不会报错,但查询界面上所有题目的内容都会变成undefined,这个在初学者源码里非常常见。

下面是导入前的一个校验小命令,可选:

python -m json.tool questions.json > /dev/null && echo "json valid"

如果这一条都过不了,说明文件本身有问题,不用继续往RDB里导。这样至少在交付前能确认题目资源可被解析。

6. 把刷题APP源代码跑起来后的3个验证技巧

源代码项目只有在自己手上跑通、能改、能交付,才算真正理解。以下几个技巧是我拿到这类刷题APP代码后必做的前置检查。

6.1 用索引页的“刷新按钮”验证RDB是否成功导入

Index.ets里加一个临时按钮,点击后从question表里count并显示结果。如果显示的不是题目总数,说明rawfile导入失败。常见的替代方案是直接查wrong_book空表,但空表无法区分“导入失败”还是“确实没有数据”。因此用select count(*) as count from question配合一个日志输出,可以在5分钟内定位80%的数据问题。

6.2 用hdc命令查日志和拉取数据库文件

调试刷题APP时,DevEco的log窗口信息太杂,可以单独使用hdc命令过滤应用日志:

hdc shell "hilog | grep quiz_app"

如果怀疑RDB建表失败,可以从设备拉库出来检查:

hdc file recv /data/app/el2/100/base/com.example.quiz/haps/entry/files/databases/quiz.db ./quiz.db

拿到本地后用SQLite工具查看表结构和行数,比在IDE里面猜快很多。注意受试设备路径可能因版本不同略有差异,上面路径是HarmonyOS 4常见的应用私有目录,如果找不到可以在设备上执行hdc shell find /data/app -name "quiz.db"

6.3 验证签名配置

最后一步不是“部署发布”,而是确认build-profile.json5里的签名配置指向的是自己的证书。很多源代码包附带的是作者本人的发布证书,安装到自己手机时会报signature verification failed。去File > Project Structure > Signing Configs里重新勾选“Automatically generate certificate”,然后重新构建。这一步做完,才算真正接管了这个刷题APP源代码。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 23:04:34

YOLOv10快递包装缺陷检测实战指南

简介&#xff1a;本资源面向计算机视觉方向的算法工程师、AI初学者及工业质检场景开发者&#xff0c;提供基于YOLOv10的快递包裹与包装盒缺陷检测完整解决方案。资源包含已训练好的高精度检测权重模型&#xff0c;支持开箱即用的推理部署&#xff1b;同时配套1200余张真实场景采…

作者头像 李华
网站建设 2026/9/11 23:00:15

基于Hadoop的电影推荐系统:MapReduce协同过滤实现与课程设计指南

简介&#xff1a;基于Hadoop的电影推荐系统设计与实现方案&#xff0c;适合大数据、计算机相关专业学生作为小组作业、课程设计或毕业设计参考&#xff0c;也可供初学者了解推荐系统与Hadoop生态的结合方式。方案围绕电影评分数据&#xff0c;实现基于用户或物品的协同过滤推荐…

作者头像 李华
网站建设 2026/9/11 22:59:42

STM32F4 I2C实战:硬件I2C与软件模拟I2C选型与总线锁死恢复

简介&#xff1a;面向STM32F4X嵌入式开发者&#xff0c;这份压缩包提供了I2C总线通信的完整实现&#xff0c;同时给出硬件外设驱动与软件模拟两条路径&#xff0c;适合需要掌握不同I2C实现方式的中级开发者参考。包内共2个文件&#xff0c;分别是程序源码I2C.c和配套头文件I2C.…

作者头像 李华
网站建设 2026/9/11 22:59:09

基于CNN的人脸表情识别实战:从FER-2013到实时部署

简介&#xff1a;本资源是一套完整的人脸表情识别毕业设计实战项目&#xff0c;面向计算机及相关专业本科生&#xff0c;助力毕业设计选题、实现与答辩全流程。项目基于Python与卷积神经网络&#xff08;CNN/VGG/ResNet&#xff09;&#xff0c;涵盖数据预处理、多模型训练对比…

作者头像 李华
网站建设 2026/9/11 22:56:56

大模型商业化困境与广告变现技术解析

1. 大模型商业化的现实困境那天看到ChatGPT开始推送广告的消息&#xff0c;我正和几个做AI产品的同行在咖啡馆闲聊。一位做NLP的老工程师突然放下手机说&#xff1a;"OpenAI终于还是走到这一步了。"这句话瞬间引发了热烈讨论——大家其实都心知肚明&#xff0c;像Cha…

作者头像 李华