【无标题手把手教你用 ArkTS 构建 HarmonyOS 记账应用

手把手教你用 ArkTS 构建 HarmonyOS 记账应用
一、前言
2024 年华为正式发布了 HarmonyOS NEXT 系统,"纯血鸿蒙"时代已经到来。对于开发者,使用 ArkTS + ArkUI 构建原生鸿蒙应用正在成为主流。
最近我基于 HarmonyOS NEXT(API 26)开发了一款名为**“老奶奶记账”**的小应用。它虽简单,却涵盖了 HarmonyOS 开发的核心知识点:Ability 生命周期、RDB 数据库、声明式 UI、弹窗交互等。本文将从项目结构、数据层、UI 层、交互逻辑四个方面深入剖析,为正在学习鸿蒙开发的朋友提供一份实战参考。
二、项目概览
| 项目 | 内容 |
|---|---|
| 应用名称 | 老奶奶记账 |
| BundleName | com.example.project |
| 目标 SDK | 26.0.0(HarmonyOS NEXT) |
| 数据存储 | RDB(关系型数据库) |
| 页面路由 | 单页面(pages/Index) |
核心功能:
- 收支记录:支持支出 / 收入两种类型
- 分类管理:8种支出分类 + 5种收入分类,配有 Emoji 图标
- 金额汇总:总收入、总支出、结余一目了然
- 记录 CRUD:添加、修改、删除记账记录
- 空状态提示:无记录时引导用户操作
- 数据持久化:本地 RDB 数据库存储
三、项目结构
project/
├── AppScope/ # 应用级配置
│ ├── app.json5 # 应用全局配置
│ └── resources/ # 应用图标和名称
│
├── entry/src/main/ets/
│ ├── database/
│ │ └── DatabaseHelper.ets # 数据库操作封装(~207 行)
│ ├── entryability/
│ │ └── EntryAbility.ets # Ability 生命周期(~49 行)
│ ├── entrybackupability/
│ │ └── EntryBackupAbility.ets # 系统备份能力
│ └── pages/
│ └── Index.ets # 主页面 UI + 逻辑(~464 行)
│
├── build-profile.json5 # 构建配置(targetSdk 26.0.0)
└── oh-package.json5 # 包管理
应用模块配置了两个组件:EntryAbility(主入口,桌面图标启动)和 EntryBackupAbility(备份扩展,支持系统级云备份)。这是一大亮点——记账数据可自动参与系统备份,用户换机时无需担心数据丢失。
四、Ability 生命周期与初始化
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 跟随系统颜色模式
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
// 初始化数据库
DatabaseHelper.setContext(this.context);
DatabaseHelper.init();
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => { /* 加载主页 */ });
}
}
关键点:
onCreate中初始化数据库:传入UIAbilityContext,数据层在 Ability 创建时准备就绪,避免页面加载后的等待setColorMode(COLOR_MODE_NOT_SET):跟随系统主题,尊重用户偏好onWindowStageCreate加载页面:路径'pages/Index'与main_pages.json配置对应
五、数据层:DatabaseHelper 详解
数据库使用 @ohos.data.relationalStore,它封装了 SQLite 并提供异步 API。
5.1 数据模型
export class Record {
id: number;
amount: number; // 支出为负数,收入为正数
type: number; // 0=支出,1=收入
category: string; // 分类
note: string; // 备注
createTime: string; // "YYYY-MM-DD HH:mm"
}
一个设计巧思:金额用有符号数,支出为负、收入为正,统计时只需 SUM(amount) 即可得到结余,无需额外逻辑。
5.2 数据库初始化
static async init(): Promise<void> {
const config: relationalStore.StoreConfig = {
name: 'accountbook.db',
securityLevel: relationalStore.SecurityLevel.S1, // 最低安全等级
encrypt: false,
};
DatabaseHelper.rdbStore = await relationalStore.getRdbStore(ctx, config);
await DatabaseHelper.createTable(); // CREATE TABLE IF NOT EXISTS
}
S1 安全等级适合不包含敏感信息的应用场景,记账数据虽涉及金额,但不属于严格意义上的敏感信息。
5.3 CRUD 操作
| 方法 | 操作 | 说明 |
|---|---|---|
insert() |
INSERT | 插入记录 |
update() |
UPDATE | 按 ID 更新 |
delete() |
DELETE | 按 ID 删除 |
queryAll() |
SELECT | 时间倒序查询全部 |
queryById() |
SELECT | 按 ID 查询单条 |
sumByType() |
SUM | 按类型统计金额总和 |
值得学习的编码细节:
① 懒加载模式——所有数据操作前调用 ensureStore():
static async ensureStore(): Promise<relationalStore.RdbStore | null> {
if (DatabaseHelper.rdbStore === null) await DatabaseHelper.init();
return DatabaseHelper.rdbStore;
}
防御性编程,确保数据操作时数据库已就绪。
② 正确的资源释放——ResultSet 在 finally 中关闭:
try { /* 读取数据 */ }
finally { await resultSet.close(); }
即使读取过程抛出异常,数据库游标也能被正确释放,防止内存泄漏。
③ 游标遍历——先 goToFirstRow() 判断是否有数据,再循环 goToNextRow() 逐行读取,符合 HarmonyOS 异步语义。
六、UI 层:ArkUI 声明式构建
6.1 整体布局
Stack (Alignment.Bottom) ← 根容器,支持弹窗叠层
├── Column ← 主内容区
│ ├── 标题 "老奶奶记账"
│ ├── 统计卡片 (收入 / 支出 / 结余)
│ ├── List (记录列表 / 空状态)
│ └── Button("➕ 记一笔") ← 底部悬浮按钮
└── [Dialog - 条件渲染] ← 半透明遮罩 + 底部弹窗
6.2 响应式数据驱动
@State records: Record[] = [];
@State incomeTotal: number = 0;
@State expenseTotal: number = 0;
@State balance: number = 0;
@State 装饰的变量是响应式的——变化时 UI 自动重渲染。每次增删改操作后调用 refreshAll() 全量刷新数据:
async refreshAll(): Promise<void> {
this.records = await DatabaseHelper.queryAll();
this.incomeTotal = await DatabaseHelper.sumByType(1);
this.expenseTotal = await DatabaseHelper.sumByType(0);
this.balance = this.incomeTotal - this.expenseTotal;
}
6.3 分类与 Emoji
const EXPENSE_CATS: Category[] = [
{ name: '餐饮', emoji: String.fromCodePoint(0x1F35C) }, // 🍜
{ name: '交通', emoji: String.fromCodePoint(0x1F68C) }, // 🚌
{ name: '购物', emoji: String.fromCodePoint(0x1F6D2) }, // 🛒
{ name: '日用', emoji: String.fromCodePoint(0x1F9FB) }, // 🧻
{ name: '水电', emoji: String.fromCodePoint(0x1F4A1) }, // 💡
{ name: '娱乐', emoji: String.fromCodePoint(0x1F3AC) }, // 🎬
{ name: '医疗', emoji: String.fromCodePoint(0x1F48A) }, // 💊
{ name: '其他', emoji: String.fromCodePoint(0x1F4E6) }, // 📦
];
为什么用 String.fromCodePoint()?Emoji 在不同编辑器、编码环境中可能出现乱码,通过 Unicode Code Point 生成可以保证显示一致性。
每条记录左侧显示对应 Emoji,代替传统文字/图片图标,让界面更生动有趣。
6.4 空状态设计
if (this.records.length === 0) {
ListItem() {
Column() {
Text(String.fromCodePoint(0x1F4DD)).fontSize(72) // 📝
Text('还没有记录')
Text('点下面大按钮开始记账吧')
}
}
}
引导式空状态不仅告诉用户"没有数据",还提示下一步操作,这是优秀用户体验设计的关键一环。
6.5 记录列表项
每条记录展示为:
┌────────────────────────────────────────────┐
│ 🍜 餐饮 -¥35.00 │
│ 2024-12-25 12:30 │
│ 午餐 │
├────────────────────────────────────────────┤
│ [ 修改 ] [ 删除 ] │
└────────────────────────────────────────────┘
金额颜色按类型区分:支出红色(#C62828),收入绿色(#2E7D32),符合直觉认知。
七、Dialog 交互:新增与编辑二合一
7.1 状态驱动弹窗
@State showDialog: boolean = false;
@State isEditMode: boolean = false;
@State editTargetId: number = 0;
@State dialogAmount: string = '';
@State dialogNote: string = '';
@State dialogType: number = 0;
@State dialogCategory: string = EXPENSE_CATS[0].name;
当 showDialog 为 true 时,条件渲染遮罩层和底部 Dialog:
if (this.showDialog) {
// 半透明遮罩,点击关闭
Column() { Blank().backgroundColor(0x80000000).onClick(() => this.closeDialog()); }
// 底部弹窗内容
Column() { /* 标题 + 类型选择 + 分类 + 金额 + 备注 + 按钮 */ }
.alignSelf(ItemAlign.End)
.borderRadius({ topLeft: 20, topRight: 20 })
}
7.2 新增与编辑复用
private openAddRecord(): void { // 清空所有字段为默认值
this.isEditMode = false; this.editTargetId = 0;
this.dialogAmount = ''; this.dialogNote = '';
this.dialogType = 0; this.dialogCategory = EXPENSE_CATS[0].name;
this.showDialog = true;
}
private openEditRecord(r: Record): void { // 填充现有值
this.isEditMode = true; this.editTargetId = r.id;
this.dialogAmount = Math.abs(r.amount).toFixed(2);
this.dialogNote = r.note; this.dialogType = r.type;
this.dialogCategory = r.category; this.showDialog = true;
}
保存时根据 isEditMode 区分 insert 或 update。修改时保留原始 createTime——记账场景下的合理行为,修改金额不应该改变记录的创建时间。
7.3 类型与分类联动
private selectDialogType(t: number): void {
this.dialogType = t;
this.dialogCategory = t === 0 ? EXPENSE_CATS[0].name : INCOME_CATS[0].name;
}
切换收支类型时自动重置到对应分类的第一个选项,减少用户操作步骤。
八、工具函数
金额格式化: 超过 10000 显示为 X.XX万,否则保留两位小数,已在为大额场景做准备。
function formatMoney(n: number): string {
if (Math.abs(n) >= 10000) return (n / 10000).toFixed(2) + '万';
return n.toFixed(2);
}
时间格式化: 生成 YYYY-MM-DD HH:mm 格式字符串,支持直接字典序排序(orderByDesc)。
九、资源管理
9.1 颜色系统
主色调选择了温暖的橙色 #E65100,配合暖白色 #FFF7E6 背景,营造温馨亲切的视觉感受:
{ "primary": "#E65100", "income_color": "#2E7D32",
"expense_color": "#C62828", "app_background": "#FFF7E6" }
9.2 深色模式
资源文件按限定词目录组织,resources/dark/element/color.json 定义深色模式颜色。系统自动根据主题切换资源,代码无需额外判断。
9.3 字符串资源化
所有用户可见文本定义在 string.json,代码中通过 $r('app.string.xxx') 引用。这种做法的优势:方便国际化、统一管理、修改无需搜索代码。
十、开发心得
-
声明式 UI + 响应式状态:ArkUI 的
@State类似 SwiftUI/Compose,开发者只需关注数据变化,无需手动操作 UI -
异步无处不在:从数据库操作到窗口加载,HarmonyOS API 大量使用 Promise,
async/await是标配写法 -
资源文件提前规划:颜色、文案、字号通过资源文件管理,保证一致性,方便团队协作
-
数据库安全等级按需选择:S1 够用就不选更高等级,避免影响数据跨设备迁移
-
系统备份能力是亮点:
EntryBackupAbility实现系统级云备份,记账数据随账号自动恢复
十一、可扩展方向
- 数据图表(
@kit.ArkCharts月度趋势图/分类饼图) - 多账本支持
- 搜索与筛选(日期范围、分类、关键词)
- 数据导出(CSV/Excel)
- 生物识别锁(指纹/面部)
- 日历视图
- 预算提醒
十二、总结
"老奶奶记账"虽然只有三个核心文件(EntryAbility.ets、DatabaseHelper.ets、Index.ets),但完整展示了 HarmonyOS 应用开发的全链路:Ability 生命周期 → RDB 数据库操作 → ArkUI 声明式 UI → 资源管理 → 系统备份集成。
项目代码量约 720 行,覆盖了开发中的核心知识点和最佳实践——ResultSet 正确关闭、懒加载模式、防御性编程、条件渲染交互、深色模式适配等。
如果你正在学习 HarmonyOS 开发,建议从这个小项目开始,逐步理解这些核心概念,再向更复杂的多页面、多模块应用进发。
项目环境: HarmonyOS NEXT(API 26),DevEco Studio
源码路径: entry/src/main/ets/ 目录下全部 ArkTS 文件
本文发布于 2025 年,API 版本可能随官方更新,请以最新文档为准。
更多推荐



所有评论(0)