news 2026/8/27 2:06:05

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 9

HarmonyOS 4 对应的 API 版本是 9 和 10,考虑到大作业的兼容性,我选择了 API 9 作为编译目标。原因很实际:教程多、资料全、模拟器支持稳定,而且大多数课程实验环境装的就是这个版本。

如果后续要适配 HarmonyOS NEXT(API 12+),主要改动在两个方面:一是旧版一些组件属性被废弃,比如部分尺寸单位;二是路由写法从router迁移到Navigation的推荐写法。但核心的 ArkTS 语法和数据绑定逻辑是通用的,从 API 9 切入反而更不容易踩新版本的坑。

2.2 DevEco Studio 工程创建

打开 DevEco Studio,选 Application 模板创建项目。关键配置项如下:

  • Compile SDK:选择 API 9
  • Model:Stage(默认就是,别选 FA 模型)
  • Language:ArkTS
  • 设备类型:默认勾选 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(); } }

这里要特别注意:切换题目时,必须把selectedOptionisAnswered同时重置,不然会出现"新题上还显示着上一题的答案"这种低级 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 错题本插入与查询

错题写入的时机在刷题页已经说过。写入用insert

async 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(): Promise<Question[]> { const predicates = new relationalStore.RdbPredicates('wrong_book'); predicates.orderByDesc('create_time'); const resultSet = await this.store?.query(predicates); // 遍历 resultSet,解析 options JSON 后封装为 Question 对象 }

这里有个很容易踩的坑:RdbPredicatesorderByDesc是链式调用,但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、什么时候重置,整个答题流程就稳了。

如果你也在做类似的大作业,建议先把"最少可用版本"跑通——题库能加载、页面能跳转、题能答对——再逐步加上错题本和统计。第一版功能越简单,越容易快速验证整体链路,后面加功能也就有了信心。希望这篇拆解对你有点帮助,有问题随时交流。

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

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

IDM激活脚本使用指南:3个步骤免费锁定试用期

IDM激活脚本使用指南&#xff1a;3个步骤免费锁定试用期 【免费下载链接】IDM-Activation-Script IDM Activation & Trail Reset Script 项目地址: https://gitcode.com/gh_mirrors/id/IDM-Activation-Script 试用期倒计时还剩3天&#xff0c;你正盯着注册表发呆。I…

作者头像 李华
网站建设 2026/8/27 2:04:02

基于Matlab的数字水印技术实战:从原理到版权保护应用

1. 从一道赛题看数字时代的版权困局去年&#xff0c;我带着学生团队参加了“深圳杯”数学建模挑战赛&#xff0c;B题“电子资源版权保护问题”让我们这群技术背景的人&#xff0c;第一次系统性地思考了数字内容在互联网上裸奔的尴尬。题目本身没有给出具体的正文描述&#xff0…

作者头像 李华
网站建设 2026/8/27 2:01:45

小波阈值降噪实战:从原理到Python实现与参数调优

简介&#xff1a;在信号处理与工程实践中&#xff0c;噪声抑制是提升数据质量的核心环节。传统滤波方法难以兼顾细节保留与平滑效果&#xff0c;而小波分析凭借其时频局部化特性&#xff0c;为非平稳信号处理提供了高效路径。小波阈值降噪的基本原理是通过多层分解将信号与噪声…

作者头像 李华
网站建设 2026/8/27 2:00:32

无风风铃制作指南:Arduino与电磁铁模拟自然随机敲击

说实话&#xff0c;“Windless Wind Chimes”这名字有点迷惑性&#xff0c;字面意思是“无风的风铃”&#xff0c;但我真正做的东西是&#xff1a;在没有自然风的室内&#xff0c;用一套电磁机械装置&#xff0c;让真正的金属管发出风铃声。Part 1那版原型勉强能响&#xff0c;…

作者头像 李华
网站建设 2026/8/27 1:55:17

G6K继电器配P6K插座:从焊接改为可插拔,维护效率大幅提升

说实话&#xff0c;刚看到OMRON发布P6K插座适配G6K继电器系列这条消息的时候&#xff0c;我第一反应是“这东西早该出了”。干过设备维护或者板卡设计的朋友应该都懂&#xff0c;G6K这种超小型信号继电器虽然性能稳定、体积紧凑&#xff0c;但一旦焊死在PCB上&#xff0c;后期更…

作者头像 李华