HarmonyOS刷题App开发实战:ArkTS状态管理与RDB持久化

📅 2026/8/27 2:06:10
HarmonyOS刷题App开发实战:ArkTS状态管理与RDB持久化
简介在移动应用开发中状态管理和数据持久化是构建交互流畅、数据可靠应用的两大核心基石。状态管理负责维护界面与数据的一致性而关系型数据库RDB则提供了结构化数据的本地存储方案。理解这些基础概念对于开发任何实用型应用都至关重要。以刷题类应用为例其核心场景天然包含题库加载、答题判分、错题记录与统计展示恰好覆盖了状态管理、路由跳转、本地数据库操作等典型技术点。本文基于HarmonyOS平台通过一个完整的刷题App项目详细拆解如何利用ArkTS的声明式UI和State状态管理实现即时反馈的答题交互并借助RDB数据库完成错题本与做题记录的持久化。从工程搭建到数据库设计再到避坑实践为开发者提供了一条从理论到落地的完整路径。 刷题类 App 算是大作业里最稳的选题之一需求边界清晰功能容易量化演示效果好。我之前用 HarmonyOS 4 做过一个完整的刷题应用从题库加载、答题判分、错题收集到统计展示全部跑通直接用于课程设计提交。这篇文章把整个项目的设计思路和核心代码拆开讲适合正在做鸿蒙大作业、或者想快速入门 ArkTS 状态管理的同学参考。这个项目不依赖后端服务所有题目数据内置在应用里用本地关系型数据库做错题和收藏记录整体工程量控制在一个学期大作业的合理范围内同时又能把 HarmonyOS 的核心特性都覆盖到ArkUI 声明式 UI、状态管理、路由跳转、RDB 数据库、首选项存储。不管最后答辩展示还是写课设报告内容支撑都够用。1. 大作业选题考量与功能拆解1.1 为什么选刷题这个方向大作业选题最怕两种情况一是太小两天写完报告没东西写二是太大一个人做不完最后东拼西凑。刷题 App 刚好卡在中间——题库是现成数据逻辑只有答题和判分两条线UI 也以列表和卡片为主不涉及复杂动画和自绘组件用 ArkTS 的常规组件就能覆盖。另一个好处是刷题这个场景天然自带数据闭环。答题产生记录记录生成统计统计再反馈给用户。这让你的数据存储和数据展示两个模块有实际意义而不是为了用数据库而强行加功能。1.2 功能清单与交互流程我最终实现的功能模块分为四块题库列表按科目分类展示题目支持从 JSON 内置题库加载刷题界面单选题作答、选项即时反馈、显示答案解析错题本自动收录答错的题支持手动移除数据统计总做题数、正确率、各科目题量分布启动流程也很直观进入首页看到分类列表点击某个科目进入答题页答完一题自动进入下一题全部答完后弹出成绩结果。返回首页后统计页的数据已经自动更新。1.3 任务拆分与工作量评估这可能是大作业阶段最有用的习惯——把项目拆成可以逐天完成的子任务。我的拆分方式供参考任务模块涉及技术点预估工时工程搭建与页面骨架Stage 模型、Navigation 路由3小时题库数据结构定义与加载类、接口、JSON 解析4小时刷题页面与判题逻辑State、事件绑定、条件渲染6小时错题本与收藏RDB 数据库、增删改查8小时统计页数据聚合SQL 查询、ForEach 渲染4小时样式打磨与打包通用样式、HAP 打包3小时这样拆完每个任务的验收标准都很明确。比如刷题页面做完标准就是能看到题目、能点击选项、能判定对错。不会出现做到一半发现前后设计脱节的尴尬。2. 开发环境与工程搭建2.1 API 版本选择HarmonyOS 4 与 API 9HarmonyOS 4 对应的 API 版本是 9 和 10考虑到大作业的兼容性我选择了 API 9 作为编译目标。原因很实际教程多、资料全、模拟器支持稳定而且大多数课程实验环境装的就是这个版本。如果后续要适配 HarmonyOS NEXTAPI 12主要改动在两个方面一是旧版一些组件属性被废弃比如部分尺寸单位二是路由写法从router迁移到Navigation的推荐写法。但核心的 ArkTS 语法和数据绑定逻辑是通用的从 API 9 切入反而更不容易踩新版本的坑。2.2 DevEco Studio 工程创建打开 DevEco Studio选 Application 模板创建项目。关键配置项如下Compile SDK选择 API 9ModelStage默认就是别选 FA 模型LanguageArkTS设备类型默认勾选 Phone 即可工程创建后会自动生成entry模块这是应用的主模块代码都在这个模块下。后续要修改的module.json5文件用于声明页面路由、权限和应用信息resources/base/profile/main_pages.json则维护所有页面的路由映射。我习惯先把不需要的模板代码删掉从空白开始写这样代码结构自己心里有数。模板里的Index.ets默认带了一堆示例组件和状态变量留着反而干扰视线。2.3 模块化目录规划大作业阶段不用搞太复杂的架构但目录划分清晰能少踩很多坑。我的entry/src/main/ets目录结构ets/ ├── common/ # 常量与工具类 │ └── Constants.ets ├── model/ # 数据模型类 │ └── Question.ets ├── pages/ # 页面组件 │ ├── Index.ets # 首页题库列表 │ ├── QuizPage.ets # 刷题页 │ ├── WrongBookPage.ets # 错题本 │ ├── FavoritePage.ets # 收藏页 │ └── StatisticsPage.ets # 统计页 ├── database/ # 数据库操作封装 │ └── QuestionDB.ets └── resources/ # 题库数据 └── questions.json这个分层把第一版需要维护的文件控制在 10 个以内每个文件职责单一。答辩问起来也好阐述直接说model 层是数据结构database 层负责持久化pages 层管界面就够了。3. 题库数据与刷题核心逻辑实现3.1 题目数据模型定义刷题 App 的核心是题目实体我用一个普通类定义它// model/Question.ets export class Question { id: number; category: string; // 科目分类 content: string; // 题干 options: string[]; // 选项数组 answer: number; // 正确答案索引 explain: string; // 答案解析 constructor(id: number, category: string, content: string, options: string[], answer: number, explain: string) { this.id id; this.category category; this.content content; this.options options; this.answer answer; this.explain explain; } }大作业阶段的题目类型我全部用单选题原因很直接判分逻辑简单、数据模型好定义、界面交互集中处理。你要加多选题就得改选项交互逻辑和判分逻辑工程量会明显增加。3.2 内置题库数据的加载方式题目数据有两种常见来源一种放resources/rawfile目录通过getContext().resourceManager.getRawFileContent()读取另一种直接以 JSON 字符串的形式放在代码里。我选了后者避免多学一套资源读取 API也方便直接预览数据。// common/QuestionBank.ets import { Question } from ../model/Question; const questionsData [ { id: 1, category: HarmonyOS 基础, content: ArkTS 语言基于哪种编程语言扩展而来, options: [Java, TypeScript, C, Python], answer: 1, explain: ArkTS 是 TypeScript 的超集扩展了声明式 UI 描述能力。 }, // 更多题目... ]; export class QuestionBank { static getQuestions(): Question[] { return questionsData.map(item new Question( item.id, item.category, item.content, item.options, item.answer, item.explain ) ); } }这里有个实用建议题目数量不用太多我放了 30 题每个科目 10 题足够演示。但要保证每个科目的题量是 5 的倍数这样统计页的百分比和图表更整齐。3.3 刷题页面的状态管理设计刷题页是整个 App 的关键页面也是 ArkTS 状态管理应用最集中的地方。我用三个核心状态变量State currentIndex: number 0; // 当前题目索引 State selectedOption: number -1; // 用户选择的选项索引 State isAnswered: boolean false; // 当前题是否已作答 State correctCount: number 0; // 答对题数currentIndex控制题目切换selectedOption记录用户点击了哪个选项isAnswered控制选中后是否立即判题并展示解析correctCount累计正确数最后成绩页要用。这里面最关键的细节是选项点击事件的逻辑。用户点选一个选项后要立即判题并显示对错反馈同时禁用其他选项的点击避免连续点击导致状态错乱。// pages/QuizPage.ets 内点击选项的处理函数 onOptionClick(optionIndex: number) { if (this.isAnswered) { return; // 已作答防止重复点击 } this.selectedOption optionIndex; this.isAnswered true; if (optionIndex this.questions[this.currentIndex].answer) { this.correctCount; // 答对不做额外记录 } else { // 答错写入错题本 this.questionDB.insertWrong(this.questions[this.currentIndex]); } }这样设计的好处是逻辑边界清晰用户一次作答只触发一次状态变更判分和错题写入都集中在一个函数里完成排查问题只需要盯这个函数。3.4 题目切换和进度条每次答题完成后界面会显示一个下一题按钮用户点击后推进索引nextQuestion() { if (this.currentIndex this.questions.length - 1) { this.currentIndex; this.selectedOption -1; this.isAnswered false; } else { // 全部答完保存数据并跳转统计页 this.routerToStatistics(); } }这里要特别注意切换题目时必须把selectedOption和isAnswered同时重置不然会出现新题上还显示着上一题的答案这种低级 bug。我第一次没重置selectedOption结果新题默认选中了上一题的位置。UI 上的进度反馈我用了Progress组件绑定进度值Progress({ value: (this.currentIndex 1) / this.questions.length * 100, type: ProgressType.Linear })这个组件不用手动刷新value绑定的状态变量一变进度条自动更新ArkUI 的响应式特性在这里体现得很直观。4. 错题本、收藏与数据持久化4.1 关系型数据库 RDB 建表错题本和收藏功能如果只存在内存里App 一关就没了所以必须落盘。HarmonyOS 提供两个持久化方案首选项Preferences和关系型数据库RDB。首选项适合存键值对比如用户设置、学习进度错题和收藏这种结构化数据列表用 RDB 更合适。我封装了一个QuestionDB类在对象初始化时建表// database/QuestionDB.ets import relationalStore from ohos.data.relationalStore; import { Question } from ../model/Question; export class QuestionDB { private store: relationalStore.RdbStore | null null; async init(context: Context) { const config: relationalStore.StoreConfig { name: quiz_app.db, securityLevel: relationalStore.SecurityLevel.S1 }; this.store await relationalStore.getRdbStore(context, config); await this.store.executeSql( CREATE TABLE IF NOT EXISTS wrong_book ( id INTEGER PRIMARY KEY AUTOINCREMENT, question_id INTEGER NOT NULL, category TEXT NOT NULL, content TEXT NOT NULL, options TEXT NOT NULL, answer INTEGER NOT NULL, explain TEXT, create_time INTEGER NOT NULL ) ); // 收藏表结构类似稍作调整 } }建表语句里最需要注意的是options是数组不能直接以数组类型存入数据库我把它转成 JSON 字符串存 TEXT 字段读出来再JSON.parse()还原。这是所有本地存储结构化数组的统一做法。4.2 错题本插入与查询错题写入的时机在刷题页已经说过。写入用insertasync insertWrong(question: Question) { const values: relationalStore.ValuesBucket { question_id: question.id, category: question.category, content: question.content, options: JSON.stringify(question.options), answer: question.answer, explain: question.explain, create_time: Date.now() }; await this.store?.insert(wrong_book, values); }查询错题列表时按时间倒序排列最近的错题排最前面async queryWrongList(): PromiseQuestion[] { const predicates new relationalStore.RdbPredicates(wrong_book); predicates.orderByDesc(create_time); const resultSet await this.store?.query(predicates); // 遍历 resultSet解析 options JSON 后封装为 Question 对象 }这里有个很容易踩的坑RdbPredicates的orderByDesc是链式调用但query返回的ResultSet必须手动goToNextRow()遍历遍历完记得close()释放。我之前忘了关ResultSet连续打开错题本几次后 App 直接崩溃日志报Too many open resultSets。4.3 统计页面的 SQL 聚合统计页要展示总做题数、正确率、各科目正确率等指标。这些数据如果每次都把所有记录查出来自己算效率低且代码冗长。RDB 支持 SQL 原生查询直接聚合async getTotalStats(): Promise{ total: number, correct: number, rate: number } { const sql SELECT COUNT(*) AS total, SUM(CASE WHEN is_correct 1 THEN 1 ELSE 0 END) AS correct FROM answer_records; const resultSet await this.store?.querySql(sql); // 解析结果 }统计页我选择了三个主要展示模块总题数和正确率大数字、每日做题量的柱状图、各科目正确率对比。柱状图我用Row布局加不同高度的Column模拟不需要引入第三方图表库图形效果也够课设展示。4.4 做题记录表结构设计一开始我只设计了三张表题库表、错题表、收藏表。后来做统计时发现漏了关键的做题记录表——只有知道每次答题的时间、结果才能算每日做题趋势。于是补了第四张表CREATE TABLE IF NOT EXISTS answer_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, question_id INTEGER NOT NULL, category TEXT NOT NULL, is_correct INTEGER NOT NULL, answer_time INTEGER NOT NULL )每答一题就插入一条记录统计页按answer_time分组查询就能得到趋势数据。这个需求的补丁过程让我意识到设计阶段把要展示什么统计指标想清楚再反推需要什么数据表比先建表再想展示什么要靠谱得多。5. 常见问题排查与避坑实录5.1 页面无法跳转和参数传递我在实现首页点击某个分类进入刷题页时用了router.pushUrl但参数传递一开始很混乱。HarmonyOS 的路由参数是字符串不能直接传对象我最初试着把一个Question[]数组整个传过去结果页面跳过去后拿到的是[object Object]。正确的做法是传简单的索引或 ID另一个页面通过查询获取完整数据。router.pushUrl({ url: pages/QuizPage, params: { category: HarmonyOS 基础 } });然后刷题页在aboutToAppear()生命周期里读取参数根据分类从题库类里筛选题目。5.2 状态变量更新了但 UI 不刷新这是初学者最容易碰到的问题。我遇到过列表删除了一条错题记录State变量也更新了但页面上这条记录还在。原因是我直接修改了State数组的某个元素而不是重新赋值一个新数组。// 错误写法 this.wrongList.splice(index, 1); // 直接操作原数组 // 正确写法 this.wrongList this.wrongList.filter(item item.id ! id);ArkUI 的响应式系统监听的是数组引用变化直接修改数组内容不会触发ForEach重新渲染。我记得第一次排查这个问题花了近半天翻文档才发现官方明确说明要整体替换。5.3 模拟器和真机表现不一致开发后期我在 Previewer 预览和模拟器上测试都没问题但一装到真机上发现文字偏小、点击区域不灵敏。后来定位到原因Previewer 默认屏幕密度和处理逻辑与真机有差异vp单位在真机上渲染时按屏幕密度动态换算而不是按 Previewer 的固定比例。建议从开发第一天就用模拟器验证创建工程时选模拟器模板每写完一个功能点就在模拟器上跑一遍。模拟器响应的布局和真机基本一致能提前暴露大部分兼容性问题。5.4 数据库文件不存在的排查有时启动 App 会报table wrong_book does not exist这类错误通常是因为init()在页面使用数据库之前没有完成异步调用。我最初在aboutToAppear()里没有await数据库初始化就立即调用了queryWrongList()结果拿到一个空的数据库句柄。解决方案是在入口页面的onPageShow()里先await db.init(context)确保数据库就绪后再进入其他页面或者延迟到用户真正点击进入错题本时再初始化。5.5 打包签名问题大作业提交时老师一般要求能生成可安装的 HAP 文件。DevEco Studio 里打包要配置签名信息默认是自动签名但有时会因本地未配置华为账号而失败。一个省事的方式是先用 debug 签名打包给模拟器安装最后交作业前再处理 release 签名。我是这么处理的全程用自动签名调试最后一两天申请了个调试证书配置好 profile 后打出 release HAP。整个配置流程不复杂跟着 DevEco 的引导走就行就是注意别把证书文件传进代码仓库答辩时容易被扣分。6. 课设报告与演示的有用建议6.1 架构图不用画太复杂课设报告里需要画系统结构图。很多同学喜欢画那种五六层的企业级架构图但大作业项目实际只有 UI 层、数据层和工具类画太多层反而显得虚。坦诚地画三层UI 层页面组件 状态管理、逻辑层业务逻辑封装、数据层题库数据 RDB 存储这是这个项目真实的结构。6.2 演示脚本比代码更重要答辩演示时最容易出的问题是现场操作时找不到某个入口或者题目顺序忘了。我建议提前写好一份演示脚本把每个功能点的操作路径写清楚。我第一次演示时因为紧张点错了科目结果跳到了一个空题库页面场面非常尴尬。写脚本后在电脑上完整跑一遍能大幅减少演示事故。6.3 扩展方向的自然延伸如果老师问你这个项目还能怎么改进可以准备几个合理方向自选题目功能允许用户录入新题并同步到题库本地学习记录导出把错题导出为文本或 PDF添加分类筛选和随机刷题模式接入分布式能力手机和平板同步学习进度这些方向都不用推翻现有架构在现有数据层和 UI 层上做增量回答起来也有底气。7. 最后一点个人体会做完这个项目再回头看最有价值的收获不是代码量而是把需求到实现的路径理顺了。先想清楚功能边界和数据结构再写代码比边写边想效率高得多。特别是状态管理那块想清楚哪些数据需要State、什么时候重置整个答题流程就稳了。如果你也在做类似的大作业建议先把最少可用版本跑通——题库能加载、页面能跳转、题能答对——再逐步加上错题本和统计。第一版功能越简单越容易快速验证整体链路后面加功能也就有了信心。希望这篇拆解对你有点帮助有问题随时交流。本文还有配套的精品资源点击获取