☰
鸿蒙开发实战:半小时用ArkTS+RelationalStore构建记事本App
2026/10/17 19:07:37 网站建设 项目流程

最近被问得最多的一个问题就是:鸿蒙应用开发到底难不难?我的回答通常是:先别管难不难,你花半小时做一个记事本试试水。记事本这个项目属于典型的入门练手Demo,功能边界清晰、页面不多、代码量可控,但这一个App却完整覆盖了列表展示、页面路由、表单交互、数据本地持久化这些核心开发技能。再加上现在AI辅助开发工具已经相当成熟,用自然语言描述需求就能生成大部分骨架代码,从新建工程到程序跑起来,半小时绰绰有余。这篇文章就把我这次实战的完整过程记录下来,从怎么拆需求、怎么选技术方案,到代码怎么落地、踩了哪些坑,一步一步讲清楚。无论你是第一次接触鸿蒙开发,还是已经在其他平台写过App想快速迁移过来,这篇内容都能帮你快速建立对鸿蒙开发的全链路认知。

1. 项目定位:清醒认识记事本App的学习价值

1.1 一张图的业务拆解:列表、编辑与数据存储

很多人一听到"记事本"就觉得太简单,提不起兴趣。但我要说的是,记事本恰恰是学习一个应用开发框架时性价比最高的项目。因为它的功能虽然小,却把业务App最核心的三个层次全部串起来了。

第一层是数据层,涉及持久化方案选型、建表语句设计、增删改查封装,这直接决定了你的数据能不能稳妥地落盘、能不能按预期查出来。第二层是页面层,至少需要一个首页列表和一个编辑页,涉及声明式UI的组件搭建、状态变量的管理、列表渲染方式。第三层是交互层,包括页面跳转、参数传递、返回后数据刷新、删除确认之类的操作反馈。

我在动手之前先把记事本的最小可用功能定成四条:新建笔记、编辑笔记、删除笔记、首页列表展示。新建和编辑共用同一个编辑页,通过路由参数区分是新增还是修改。首页列表按更新时间倒序排列,让最近编辑的笔记排在最前面。删除我就做了个列表项上的删除按钮,没有做左滑手势,因为半小时内没必要为了交互炫技增加额外复杂度。

这四条功能听起来不多,但每一条背后都对应一个技术点。新建和编辑对应数据插入与更新,列表展示对应全量查询与UI渲染,删除对应条件删除操作。把这些跑通,你就已经掌握了鸿蒙应用开发一天里用的最多的那几条开发路径。后续不管是做待办清单、记账本、日记本,还是做收藏夹,本质上都是在这个骨架上换皮肤。

1.2 技术选型对比:为什么是ArkTS + RelationalStore

当前鸿蒙应用开发的主流技术组合是ArkTS语言配合ArkUI声明式框架。ArkTS可以理解为TypeScript的超集,在标准TS语法的基础上增加了明确的类型约束和装饰器支持,如果你写过TypeScript或者JavaScript,上手成本很低。ArkUI的写法则和SwiftUI、Jetpack Compose这类声明式UI框架高度相似,用组件树描述界面结构,用状态装饰器来控制UI刷新,天然适合快速搭建页面。

存数据这块,鸿蒙提供了不止一种方案,我选型时专门做了一轮对比,下面这个表格可以直接拿走参考:

存储方案适用场景主要特点
Preferences(首选项)键值对数据,比如用户设置、开关状态轻量、读写简单、适合少量数据,不适合做结构化查询
RelationalStore(关系型数据库)结构化数据,比如笔记列表、订单记录基于SQLite引擎,支持SQL建表、RdbPredicates条件查询,异步API
分布式数据服务多端协同、跨设备共享数据能力最强但配置复杂,学习成本高,入门项目用不上

记事本的数据天然是结构化的,一条笔记有标题、正文、创建时间、更新时间几个字段,还要按更新时间排序、按id精确查找,这种场景用RelationalStore最合适。我见过一些初学者图省事,把笔记对象序列化成JSON字符串塞进Preferences,短期看确实能跑,但后面一旦要加字段、要排序、要模糊搜索,就会非常痛苦,到时候还得回过头来迁数据库。与其走弯路,不如第一步就选对路子。

1.3 数据表设计:命名规范与字段规划

数据库表结构我设计得很朴素,但越朴素的表设计越能看出基本功。notes表一共五个字段:id作为主键自增,title存标题,content存正文,create_time存创建时间戳,update_time存更新时间戳。时间字段我统一用整数型存毫秒时间戳,而不是用字符串存人类可读时间,这样排序、比较、格式化都方便,展示时再通过工具函数转成想要的格式。

这里有一个命名规范的细节值得注意:数据库表字段用snake_case(下划线命名),而ArkTS类的属性用camelCase(驼峰命名)。也就是说,表里是create_time,代码类里是createTime,转换时要在ResultSet读取和ValuesBucket写入这两个方向分别映射。这个约定不是鸿蒙特有的,凡是关系型数据库配面向对象语言都有这个问题,习惯了就好。

CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, create_time INTEGER, update_time INTEGER );

id字段的AUTOINCREMENT可以保证每次插入都生成一个单调递增且不重复的主键,后续做编辑和删除时,直接拿着这个id当条件,逻辑非常干净。title字段我加了NOT NULL约束,因为一条笔记可以没正文,但至少得有个标题,否则列表页都没法展示。实际代码里我对空标题做了兜底,显示成"无标题"四个字。

2. 从空白工程到可运行骨架

2.1 环境准备:DevEco Studio的安装与SDK配置

先说一下我这次用的开发环境。官方IDE是DevEco Studio,我建议直接装最新稳定版,因为鸿蒙的SDK迭代比较快,老版本经常会遇到构建工具链和API不匹配的问题。安装过程本身不复杂,重点有三个:第一,安装时尽量勾选随IDE一起下载的HarmonyOS SDK组件,避免装完才发现少东西;第二,首次启动后需要配置SDK路径,如果安装时没下载完整,可以在SDK Manager里手动补齐;第三,模拟器镜像比较大,第一次创建时下载会比较久,建议提前把Device Manager里的模拟器创建好,并完成系统镜像下载。

工欲善其事必先利其器,模拟器这块特别提醒一句:模拟器启动后对电脑内存和CPU的占用都不小,如果你是像我一样开着浏览器、聊天软件、IDE一起干活,最好给模拟器留够资源。我这次实战用的就是模拟器,第一次冷启动花了接近一分钟,一度以为是卡死了,其实是在加载系统镜像,耐心等一会儿就好。如果你手头有鸿蒙真机,走真机调试反而更稳,只需要在开发者选项里打开调试模式,数据线连接后在IDE里选择对应设备,构建产物会自动装上去。

2.2 创建Empty Ability工程与项目结构解读

环境就绪后就是创建工程。DevEco Studio里点Create Project,选择Application下的Empty Ability模板,这个模板会生成一个最基础的Hello World页面,是我们这次开发的最小起点。工程名我填的是NotesDemo,包名用了com.example.notesdemo,设备类型这里选Phone,语言默认就是ArkTS,一路下一步就能完成创建。

创建完工程,建议先花两分钟把目录结构过一遍,不然后面写代码时容易迷路。AppScope下面的是应用全局配置,比如应用名称和图标。entry/src/main/module.json5是模块的配置文件,页面路由表就在这里声明,后面新增EditorPage页面必须在这里注册。entry/src/main/ets/pages目录放的是页面文件,默认只有一个Index.ets。EntryAbility.ets则是应用入口Ability,里面定义了Ability从创建到销毁的完整生命周期。

我拿到这个模板后的第一件事,是把默认的Hello World界面代码删掉,换成一个空白的Column容器,然后确认模拟器能装上这个空白应用。这一步看着没用,其实非常关键——它验证了签名、打包、安装、启动这一整条链路是通的。以后做任何功能,都应该先验证一次空工程能跑通,否则代码写了一大坨再发现构建环境有问题,排查起来会非常痛苦。

2.3 用AI一次性生成数据层骨架

工程骨架有了,接下来就轮到AI上场。我这次的思路是:数据和数据库相关的代码,直接让AI先生成完整版本,因为这部分逻辑模式固定、API调用相对标准,AI的容错率最高;UI部分则一半靠AI生成一半手动调整,因为页面样式和交互细节需要不断微调,完全靠AI反而不灵活。

我给AI的提示词里写清楚了这几样信息:工程是DevEco Studio创建的ArkTS工程;数据库方案用RelationalStore;数据库名是notes.db,表名是notes;字段列表详细到字段名和类型;需要的方法包括初始化建表、插入、查询全部、按id更新、按id删除。提示词写得越具体,AI生成代码的可用率越高,这段具体提示词我放在后面专门一章展开讲。

AI返回的代码骨架质量比我预期的要高,方法签名基本正确,但有几个细节需要手动修:一是它把建表SQL写在了getRdbStore之前,这会导致表还不存在就执行SQL;二是ResultSet遍历结束后没有调用close释放资源;三是部分方法返回类型和实际异步链路的类型对不上。这些小问题不算伤筋动骨,但确实暴露了AI只能保证整体结构对、不能保证边界细节都对的现状。修复之后,数据层代码我直接用了。

3. 数据存储层:记事本的地基工程

3.1 实体类与建表SQL的对应关系

数据层是记事本的底座,我按照实体类、数据库辅助类、业务调用方三层来组织。实体类就是一个普通的ArkTS类,字段对应表结构,用来在页面和数据库之间传递结构化数据。

export class Note { id: number = 0; title: string = ''; content: string = ''; createTime: number = 0; updateTime: number = 0; }

这个类没有任何装饰器或继承关系,就是纯粹的数据载体。为什么不用接口?因为ArkTS对接口的字段初始化约束更严格,类可以方便地给所有字段提供默认值,在从ResultSet逐行解析时不容易出现未初始化的问题。而且后续如果要在注解里用@State修饰,类对象也更自然。

建表SQL和实体类字段的对应关系,用一句话概括就是:表字段是数据库视角,类字段是程序视角,两者靠转换函数连接。我在写插入方法时往ValuesBucket里塞的是camelCase的key?不对,ValuesBucket的key必须和表的列名一致,也就是snake_case。这是新手最容易栽的地方——你以为传个title就行,数据库也确实能识别,但一旦字段名改成create_time而代码里写createTime,运行时会直接报字段不存在。所以我在代码里专门写了注释提醒自己:ValuesBucket、RdbPredicates、ResultSet三个位置全部使用snake_case列名。

3.2 RdbStore的初始化:Context从哪来

关系型数据库的句柄是RdbStore,获取RdbStore必须通过RelationalStore模块的getRdbStore方法,而这个方法需要一个Context参数。刚接触鸿蒙的人最困惑的就是这个Context从哪来、该不该传。

我实际使用的方案是在入口模块的onWindowStageCreate生命周期里,把应用的Context保存下来,后续所有数据库操作都复用这个Context。同时提供一个初始化方法,确保数据库和表结构只创建一次。这样设计的好处是,页面里不需要关心Context的获取,直接调用数据层暴露的方法就行。

import { relationalStore } from '@kit.ArkData'; import { common } from '@kit.AbilityKit'; import { Note } from '../model/Note'; const DB_NAME = 'notes.db'; const TABLE_NAME = 'notes'; export class NoteRepository { private static store: relationalStore.RdbStore | null = null; static async init(context: common.UIAbilityContext): Promise<void> { const config: relationalStore.StoreConfig = { name: DB_NAME, securityLevel: relationalStore.SecurityLevel.S1 }; NoteRepository.store = await relationalStore.getRdbStore(context, config); const createTableSql = 'CREATE TABLE IF NOT EXISTS notes (' + 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'title TEXT NOT NULL, ' + 'content TEXT, ' + 'create_time INTEGER, ' + 'update_time INTEGER)'; await NoteRepository.store.executeSql(createTableSql); } }

这里调用getRdbStore的时机值得注意:它返回的是一个Promise,必须await完成后store才可用。如果初始化方法和业务方法并发执行,会出现store还是null就调用了insert,直接抛空指针。所以我在初始化方法内部也加了保护,同时建议页面在aboutToAppear里先await init再加载数据列表。

3.3 增删改查方法的完整实现与边界处理

数据层的增删改查我封装成了四个静态方法。插入方法接收标题和正文,自动填充两次时间戳,返回插入后的行号;查询方法按更新时间倒序返回全部笔记数组;更新方法按id匹配,只更新标题、正文和更新时间;删除方法按id精确删除一条记录。

static async insert(title: string, content: string): Promise<number> { const values: relationalStore.ValuesBucket = { title: title, content: content, create_time: Date.now(), update_time: Date.now() }; return await NoteRepository.store!.insert(TABLE_NAME, values); } static async queryAll(): Promise<Note[]> { const predicates = new relationalStore.RdbPredicates(TABLE_NAME); predicates.orderByDesc('update_time'); const resultSet = await NoteRepository.store!.query(predicates); const notes: Note[] = []; while (resultSet.goToNextRow()) { const note = new Note(); note.id = resultSet.getLong(resultSet.getColumnIndex('id')); note.title = resultSet.getString(resultSet.getColumnIndex('title')); note.content = resultSet.getString(resultSet.getColumnIndex('content')); note.createTime = resultSet.getLong(resultSet.getColumnIndex('create_time')); note.updateTime = resultSet.getLong(resultSet.getColumnIndex('update_time')); notes.push(note); } resultSet.close(); return notes; }

几个容易被忽略的边界点我在这里集中说一下。第一,queryAll里必须用resultSet.close()释放游标资源,否则多次查询后模拟器会报内存资源耗尽,这个错误最坑的地方在于它不是必现的,往往积压到后面才炸。第二,getColumnIndex获取列下标,不要在循环里重复调用,我这里的写法其实还可以优化成循环前先缓存列下标,数据量小的时候无所谓,数据量大时性能差别明显。第三,删除方法里RdbPredicates通过equalTo('id', id)指定条件,如果id传了一个不存在的值,delete不会报错,只是影响行数为0,所以调用方不需要特殊处理。

4. 界面与交互:从数据到可视化的最后一公里

4.1 首页列表的声明式写法:@State与ForEach的配合

数据层完成后,开发重心转到了UI。首页是整个应用的第一个界面,它的职责是在页面显示时从数据库拉取全部笔记,然后用列表的形式渲染出来。ArkUI里列表渲染的核心组合是List组件加ForEach循环,外围再用Column容器做整体布局。

@Entry @Component struct Index { @State notes: Note[] = []; aboutToAppear() { this.loadNotes(); } onPageShow() { this.loadNotes(); } async loadNotes() { const data = await NoteRepository.queryAll(); this.notes = data; } }

这里有一个我在实际开发中确认过的关键点:onPageShow。aboutToAppear只在页面第一次创建时触发,如果编辑页保存后通过router.back()返回首页,aboutToAppear不会再次执行,数据就不会刷新。而onPageShow这个生命周期方法在页面每次从后台切换到前台、以及从子页面返回时都会触发。所以我同时挂了aboutToAppear和onPageShow,前者负责第一次进入时的快速加载,后者负责返回时的数据刷新。这是很多教程不会写到的细节,但它直接决定了App好不好用。

列表渲染的写法是List套ForEach,数据项是Note对象,key生成器返回note.id.toString()。这个key很重要,它让框架在数据变化时精准复用列表项而不是全部重建。界面上每一个笔记项就是一张卡片,展示标题和更新时间两个信息,右侧放删除按钮。

4.2 自定义笔记卡片组件与删除事件

卡片我单独抽了一个子组件NoteCard,这样写的好处是列表项的布局逻辑不会污染页面主文件。子组件接收两个参数:一个是Note对象,通过@Prop装饰器传入;另一个是删除回调,用普通的函数属性传入。

@Component struct NoteCard { @Prop note: Note; onDelete: () => void = () => {}; build() { Row() { Column() { Text(this.note.title.length > 0 ? this.note.title : '无标题') .fontSize(16) .fontWeight(FontWeight.Medium) .maxLines(1) Text(this.formatTime(this.note.updateTime)) .fontSize(12) .fontColor('#999999') .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Button('删除') .fontSize(12) .backgroundColor('#F0F0F0') .fontColor('#E84026') .onClick(() => this.onDelete()) } .padding(12) .backgroundColor('#F5F5F5') .borderRadius(12) } }

在父子组件数据传递时,@State和@Prop的区别要拎清楚。父组件的@State notes数组变化后,子组件的@Prop note会同步拿到新值并触发重建。但如果我在子组件里直接修改this.note.title,是不会反向同步回父组件的,因为@Prop是单向数据流。这个设计避免了很多复杂的双向绑定问题,但也意味着如果你想让子组件修改父组件的数据,唯一正确的做法就是通过回调函数把事件抛出去。

删除按钮的点击事件就是在父组件里传入的onDelete回调,父组件拿到note后调用NoteRepository.delete方法,然后重新拉取列表。这里有个更优雅的方案是用系统提供的SwipeAction组件做左滑删除,但考虑到演示的简洁性,按钮方案足够清晰,而且交互路径也更直白。

4.3 编辑页的双输入框设计与保存逻辑

编辑页是整个应用中交互最密集的页面。它需要处理两个场景:新建笔记和编辑已有笔记。同一套UI同时兼容两种场景,靠的是路由参数里的noteId——如果noteId有值就按id查出来回填,没有值就当作新笔记处理。

标题输入我用TextInput组件,正文输入用TextArea组件,两个组件都绑定对应的@State变量。这里有个ArkUI的细节:TextInput的text参数一旦绑定状态变量,后续必须通过onChange回调手动更新变量,否则界面输入不会产生任何效果。这个写法和React里的受控组件非常像,理解了这个类比就不会写错。

TextInput({ placeholder: '请输入标题', text: this.title }) .onChange((value: string) => { this.title = value; })

保存按钮的onClick里加一个保护判断:如果noteId等于-1,就进入新建分支,调用NoteRepository.insert;否则进入更新分支,调用NoteRepository.update。保存成功后直接router.back()回到首页,首页的onPageShow会自动拉最新数据。我没有做复杂的表单校验,只要求标题不能为空,空标题会被NoteCard兜底显示成"无标题"。

页面顶部的返回按钮用系统导航栏自带的返回手势即可,不需要额外实现。但如果你不想用router.pushUrl打开页面时带默认的页面切换动画,可以在路由参数里配置动画选项,这些属于体验优化,不是必须。

4.4 页面路由的参数传递与返回刷新

路由这块我用的鸿蒙标准router模块,首页跳转编辑页时用pushUrl,编辑页返回首页时用back。传递noteId的方式是在pushUrl的params字段塞进一个对象,编辑页通过router.getParams()把参数取回来。

要注意的是router.getParams()返回的是Object类型,使用前需要做类型断言。我这次传的是一个数字,直接断言成Record<string, number>然后读取noteId字段。如果传的对象结构复杂,更推荐先定义一个接口来描述params的完整结构,再在取出时做一次as操作,这样代码可读性和类型安全性都更好。

返回刷新这个环节特别容易被忽视。很多人喜欢把数据加载逻辑只放在aboutToAppear里,结果从编辑页返回后发现列表还是旧数据,就开始怀疑是不是数据库写失败了。判断办法很简单:在编辑页保存成功后打个日志,再去数据库看数据是否真的写了。数据在、界面不刷,问题就一定出在页面生命周期上。把数据加载放到onPageShow里,这个问题就自动消失了。

5. AI辅助开发实战:如何把半小时真正用满

5.1 一次生成完整代码的提示词示例

AI辅助开发最核心的技能不是写提示词,而是写"让AI不做选择题"的提示词。很多时候AI生成的东西不能用,是因为你的需求描述里模糊地带太多,AI只能靠猜,猜错概率自然高。我把这次实际用的提示词贴出来作为参考:

我在DevEco Studio中创建了一个空的ArkTS工程,入口模块是entry,包名是com.example.notesdemo。 现在要实现一个本地记事本App,请用ArkTS帮我实现数据访问层: 1. 使用@kit.ArkData中的relationalStore,数据库名notes.db,安全级别S1 2. 建表SQL:CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, create_time INTEGER, update_time INTEGER) 3. 封装NoteRepository类,提供5个静态异步方法: - init(context): 初始化数据库并建表 - insert(title, content): 插入一条笔记,返回自增id - queryAll(): 查询全部笔记,按update_time倒序,返回Note对象数组 - update(id, title, content): 按id更新 - delete(id): 按id删除 4. Note类字段:id、title、content、createTime、updateTime,全部使用默认值 5. 注意ResultSet遍历后close,方法返回Promise 请直接输出完整代码,不要解释。

这个提示词的信息密度很高,每个点都在消除歧义:工程环境一句话说明、数据库API明确到模块、建表语句直接给出、方法清单精确到方法名和签名、结果对象明确字段、编码规范明确到资源释放。AI拿到这种提示词,几乎不需要猜测,直接对着要求砌代码,可用率大幅提升。

5.2 AI生成代码的审查要点:不是无脑复制

AI生成代码后,我强烈建议不要直接复制粘贴进工程,先花三分钟做一轮快速审查。我的审查清单固定在三个维度:API名是否真实存在、异步链路是否完整、资源是否释放。API名这块,AI有时候会写出看起来像那么回事但实际不存在的方法,编译阶段就能发现,报错信息里会直接给你正确API名,照着改就行。

异步链路是重灾区。AI很习惯在方法内部用await调数据库,但它生成的方法返回值有时会漏掉Promise包装,或者在一个非async函数里用await,这会导致页面调用方拿到undefined。我审查时主要看每个方法前面有没有async关键字,内部有没有把异步结果return出来,而非只执行不返回。

资源释放是代码质量层面的审查。查询方法里如果出现resultSet,那后面基本应该跟一个close。AI经常漏掉这两行,漏掉的后果不是立刻崩溃,而是长时间运行后出现莫名其妙的异常,且不好复现。反正我在复测时发现了这个问题,手动补上关闭逻辑后才算真正达到生产级质量。

5.3 半小时时间盒:每个阶段做什么

半小时做完一件事,最大的敌人是陷入细节出不来。我给这个项目做了一个粗略的时间盒分配,实施下来节奏感很好,一共四个阶段。

0到5分钟做工程初始化:安装好DevEco Studio的就不用管这一步,直接创建Empty Ability工程,确认模拟器能用。这一步如果卡住,多半是SDK没装全,优先在SDK Manager里补装再继续。5到15分钟做数据层:用AI生成NoteRepository代码,手动修复上面说的几个坑,然后写实体类,确保编译通过。15到25分钟做UI层:先写首页列表,跑起来看到空列表,再接编辑页,把新建和跳转流程打通,最后补上删除按钮。25到30分钟做联调:重点验证三个场景——新建一条笔记保存返回、返回首页后列表刷新、删除一条笔记。

时间盒给我最大的启示是:AI辅助真正的价值不是替代你思考,而是把从零到一的时间从两三个小时压缩到半小时,但前提是你对最终代码要做什么有清晰认知。如果对全套业务逻辑完全没概念,AI反而会让时间黑洞更大,因为你连改都不知道往哪改。

6. 常见问题排查与避坑实录

6.1 编译报错几大典型场景

开发过程中编译报错基本集中在三个方面,遇到时先对号入座。第一种是SDK组件缺失或版本不匹配,报错信息里通常会出现SDK的字样,解决方式是打开SDK Manager检查Platform和Toolchains是否完整,有缺失就补齐。第二种是模块依赖或三方库没下载成功,鸿蒙项目用ohpm管理依赖,报错信息里出现ohpm install failed,多半是网络问题导致依赖下载失败,重新加载依赖或者把仓库地址换成可用源就能解决。

第三种是ArkTS语法层面的类型约束。ArkTS对类型检查比标准TypeScript更严格,AI生成的代码里偶尔会出现给变量赋了null、或在Union类型上直接调方法的情况,这类报错会在代码编辑区标红,编译器也会明确提示出问题的行号。我的经验是,编译类问题基本都是确定性的,报错信息已经把答案写在脸上了,照着修总比闷头猜强。真正难排查的是运行期问题,下一节就讲典型的运行期坑。

6.2 数据库异步回调的时序陷阱

记事本虽然小,但异步问题一样不少。最典型的一个场景是:首页加载数据时,数据还没从数据库返回,页面就已经渲染完了,结果列表空白。这是因为queryAll方法返回的是Promise,调用方必须await它并拉取到数据后再赋值给@State数组,如果只调方法不处理回调,UI自然拿不到数据。

另一个时序问题是数据库初始化。如果用户在init没有完成时就去调用insert或queryAll,会触发store为空的异常。稳妥做法是在入口Ability的onWindowStageCreate中先await NoteRepository.init,所有页面在aboutToAppear里再做数据操作。这两个层级都做await,基本能杜绝竞态问题。

还有一个我实际踩过的坑是ResultSet游标没关闭导致的后台资源泄漏。当时我在模拟器里连续开关页面好几十次,突然有一次查询返回了空结果,排查了很久才发现是前面查询没close导致游标资源耗尽。从那以后,我写数据库查询代码时都会固定检查:有没有close、有没有在return之前close。这种问题靠AI生成代码最容易漏,人工审查时务必盯着看。

6.3 状态管理刷新失效的解决经验

状态管理是ArkUI的核心机制,也是新手最容易困惑的地方。我这次遇到的现象是:删除一条笔记后,调用了this.notes = data重新赋值,界面却没有如期更新。排查下来发现原因在于@State数组的处理方式——直接push一个元素进去,框架的响应不一定能到位,但整体替换整个数组引用则一定能触发UI重建。

所以我的经验是,凡是用@State修饰数组,就尽量用整体替换的方式更新数据,比如this.notes = newArray,而不是this.notes.push(note)。这样虽然看起来少了一点声明式框架的"魔法感",但至少行为是确定的,不会出现界面和内存数据不一致的诡异问题。在子组件里,@Prop变量也不要直接改,它本来就是只读的单向同步,需要改数据就把事件抛给父组件去处理。

状态管理的另一个常见问题是频繁刷新导致的卡顿。列表数据量小的时候感受不明显,一旦数据量上百,每次保存都全量load一遍数据库再整体替换数组,界面会明显掉帧。优化思路是分页查询、局部更新,但这是进阶话题,入门阶段先保证数据正确性就好。

6.4 软键盘遮挡与页面显示问题

记事本App最核心的操作是输入文字,所以软键盘把输入框挡住的问题必须提前处理。鸿蒙的窗口默认是全屏模式,如果键盘弹出时窗口不做避让调整,底部输入框就会被键盘完全遮住,用户根本看不见自己打了什么字。

我采用的方案是在窗口配置里开启键盘避让模式。在EntryAbility的onWindowStageCreate中,获取主窗口对象后设置窗口为布局避让模式,同时把窗口的resize能力打开。这样键盘弹出时系统会自动调整整个页面布局,把输入框顶到键盘上方。不同设备上这个行为可能存在细微差异,测试时最好分别用模拟器和真机各试一轮。

另一个显示相关的小坑是列表内容被安全区遮挡。有些模拟器底部有导航条,列表最后一条笔记可能会被导航条盖住一部分。解决方法是给List底部加一点paddingBottom,或者用expandSafeArea让列表延伸进安全区后再用内边距补偿。这个属于打磨细节,但直接影响观感,值得花两分钟调好。

7. 最后的一点体会与后续扩展方向

这次半小时实战跑完,我自己最大的感受是:AI辅助开发让"入门成本"变得极低,打打字就能生成能跑的代码,但它不会自动帮你理解代码、排查问题、优化性能。真正的知识壁垒不在于代码怎么写,而在于遇到报错怎么定位、遇到数据不刷新怎么想到生命周期、遇到性能变差怎么想到资源释放。这些靠的是对框架运行机制的理解,是AI暂时替代不了的部分。

记事本这个项目本身也可以继续生长。想巩固知识,可以给它加搜索框,练模糊查询;想深入状态管理,可以加标签分类;想进一步挑战复杂交互,可以把删除改成左滑手势,再加上撤销功能。数据量变大了,就要考虑分页加载和索引优化。每加一个功能,你对鸿蒙开发的理解就又深一层。

如果你想上手鸿蒙开发,就从这个记事本开始。不要怕写出不完美的代码,先让它在模拟器里跑起来,再慢慢打磨。我这个版本也谈不上多优雅,但它完整跑通了从界面到数据再到交互的全链路,希望这份实战记录能帮你少踩一些坑,把时间花在真正值得探索的技术细节上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询