资讯动态

鸿蒙原生记事本开发实战:HarmonyOS 3+ 适配与状态管理解析

发布时间:2026/10/14 23:24:58 来源:尧图企业网站定制
简介一套基于ArkTS语言开发的鸿蒙原生记事本应用完整工程包面向HarmonyOS 3平台的初学者、课程设计学生与需要快速交付笔记工具的开发者。工程模块划分规范入口模块、基础UI组件、本地数据存储逻辑均有清晰呈现任务增删改查功能可直接运行。资源总计124个文件xml负责界面布局java承载业务与数据访问png为界面素材gradle与gradlew等构建脚本齐全压缩包大小约11.6MB已有36人下载学习。配套中文说明文档覆盖环境配置、编译步骤和常见问题可帮助零基础用户完成DevEco Studio同步与真机调试也能让有经验的开发者快速移植或扩展标签、提醒、搜索等能力。整体适合作为鸿蒙应用开发的实战参考资料。1. 鸿蒙原生记事本应用开发工程包HarmonyOS 3 适配到底解决了什么很多人拿到鸿蒙原生记事本应用开发工程包HarmonyOS 3 适配第一反应是“这不就是个 Demo 吗”。实际上当你试着把一套早期的记事本样例迁到 HarmonyOS 3 上跑会发现一堆问题关系型数据库打开失败、页面跳转后数据不刷、列表一多就卡、深色模式适配无效。这个工程包的价值不在于“能写个记事本”而在于把 HarmonyOS 3 上常见的状态管理、数据持久化、权限声明和 UI 适配都按一套可复用的方式串好了。适合谁适合要在 HarmonyOS 3 上做轻量工具类应用、又不想从空工程一点点趟坑的开发者也适合刚迁移到 ArkTS 的客户端同学拿来当工程模板。我不会假装这个工程包有什么神奇魔法它本质上是把记事本这个领域里最可靠的那套原生做法用关系型数据库存数据、用单例管理连接、用状态管理驱动 UI 刷新、用系统 API 申请权限全部白盒化。下面从工程骨架开始逐步拆开每部分怎么改、怎么调、以及我记得最清楚的几个翻车点。2. 从工程包到手跑通目录结构、导入命令与最小配置拿到工程包第一件事不是看页面代码而是先把目录能对上、能构建出来。HarmonyOS 3 的工程结构和早期版本差异很大如果拿着旧工程的 expectations 去套会直接在配置阶段卡住。2.1 工程包目录结构每个文件夹是干什么的工程包用标准的 Stage 模型入口模块是entry。一个常见的目录树长这样ProjectRoot ├── AppScope │ ├── app.json5 # 应用全局配置包名、版本号、图标 │ └── resources/base/element/string.json # 应用级字符串资源 ├── entry │ ├── src/main │ │ ├── module.json5 # 模块配置权限声明、页面路由、ability 列表 │ │ ├── ets │ │ │ ├── entryability/EntryAbility.ets # 入口 Ability │ │ │ ├── pages │ │ │ │ ├── Index.ets # 首页列表展示 │ │ │ │ ├── EditPage.ets # 编辑页新增/修改 │ │ │ └── model │ │ │ └── NoteDataSource.ets # 数据源与 RDB 封装 │ │ └── resources │ │ ├── base/element/ # 颜色、字符串、数字 │ │ └── base/media/ # 图标 │ └── build-profile.json5 # 模块构建配置 ├── build-profile.json5 # 工程级构建配置 ├── hvigorfile.ts # 构建脚本入口 └── oh-package.json5 # 三方依赖声明这个目录结构本身就是在告诉你HarmonyOS 3 应用入口不是 Activity 而是 Ability页面路径必须在pages下权限不能只写在代码里还要在module.json5中声明。工程包把数据操作放在model目录而不是堆在页面里这一点对后续维护尤其重要。常见的错误是拿到工程包直接打开Index.ets就开始改结果改了半天编译不过原因往往是module.json5里没声明需要访问的权限或者页面路由没注册。所以第一步先把目录和配置文件对齐。2.2 用 DevEco Studio 导入并构建的最小步骤DevEco Studio 打开工程包时不是选源码目录而是选包含hvigorfile.ts的根目录。导入后先同步依赖命令行可以用工程自带的工具链# 在工程根目录执行同步 oh-package.json5 中的依赖 ohpm install # 构建 HAP 包debug 模式 hvigorw assembleHap --mode module -p productdefault # 构建产物在 entry/build/default/outputs/default/entry-default-signed.hap如果本地没有配 hvigorw 环境变量直接用 DevEco Studio 的 Terminal或者点击 Sync 按钮后执行ohpm install。关键点是ohpm install必须成功否则找不到依赖库最常见的报错是ohpm install failed这时候先检查网络能访问仓库再看oh-package.json5里依赖版本是否和你本地 SDK 匹配。构建成功后再安装到真机或模拟器。模拟器建议选择 API 9 以上镜像否则ohos.data.relationalStore的行为会有差异。真机调试时记得在/resources/base/profile/main_pages.json里确认EntryAbility的页面路由包含你要启动的页面。2.3 需要手动确认的三处配置SDK、权限、签名工程包给的是通用配置落到自己手上要改三处配置项位置建议compatibleSdkVersionbuild-profile.json5保持工程包默认值至少不低于9否则relationalStore的字段不全权限声明entry/src/main/module.json5记事本应用一般只需要ohos.permission.KEEP_BACKGROUND_RUNNING可选不要加不必要权限签名证书build-profile.json5的signingConfigs工程包默认带的是调试签名真机调试要换成自己的调试证书否则安装失败这里有个血泪经验compatibleSdkVersion写高了会连带要求targetSdkVersion也改导致权限弹窗风格变化。工程包设计成兼容 HarmonyOS 3所以默认值通常取一个折衷版本。你要确认自己的设备系统版本如果设备是 HarmonyOS 3 早期版本就不要把targetSdkVersion设太高否则首页列表返回时的生命周期行为会不一样。3. 记事本核心功能拆解数据模型、RDB 存取与页面联动工程包真正值钱的不是架子而是数据层。记事本应用的本质是“增删改查 持久化”HarmonyOS 3 提供的关系型数据库接口和 Android 的 SQLite 很接近但细节上有不少差异。3.1 数据模型与建表语句别用 Preferences 存列表很多初写鸿蒙应用的人会把记事本内容存到Preferences因为接口简单。但 Preferences 本质是 KV 型键值存储不适合存可变长的列表数据更新一条笔记要整体读写数据量大时非常卡。工程包用的是relationalStore也就是关系型数据库建表语句类似 SQLCREATE TABLE IF NOT EXISTS note ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL )在 ArkTS 中执行建表需要先从上下文获取数据库路径import data_rdb from ohos.data.relationalStore; let store: data_rdb.RdbStore | undefined undefined; const STORE_CONFIG: data_rdb.StoreConfig { name: note.db, securityLevel: data_rdb.SecurityLevel.S1 }; function getStore(context: Context): Promisedata_rdb.RdbStore { return data_rdb.getRdbStore(context, STORE_CONFIG) .then((rdbStore) { store rdbStore; const SQL_CREATE CREATE TABLE IF NOT EXISTS note ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL ); return rdbStore.executeSql(SQL_CREATE).then(() rdbStore); }); }字段说明id是自增主键title和content分开存因为列表页只需要显示标题不需要把全量内容读出来。create_time和update_time用 INTEGER 存毫秒时间戳排序和计算比 TEXT 更方便。securityLevel: S1表示应用自身私有数据不上云。工程包在这里有一个典型设计数据库连接做成单例然后所有页面通过getStore()获取同一个实例避免重复打开连接导致锁等待。如果你看到自己的代码里每次增删改都去getRdbStore那一定要改掉这是后面出现“数据库 busy”的根源。3.2 首页列表与编辑页的状态同步首页用List组件展示笔记标题点击跳转到编辑页。HarmonyOS 3 的推荐做法是用State或Observed管理列表数据而不是手动调用this.list.forceUpdate()。State noteList: Note[] []; async loadNotes() { let store await getStore(this.context); let predicates new data_rdb.RdbPredicates(note); predicates.orderByDesc(update_time); let resultSet await store.query(predicates, [id, title, update_time]); let list: Note[] []; while (resultSet.goToNextRow()) { list.push({ id: resultSet.getLong(resultSet.getColumnIndex(id)), title: resultSet.getString(resultSet.getColumnIndex(title)), updateTime: resultSet.getLong(resultSet.getColumnIndex(update_time)) }); } this.noteList list; }这里的核心是RdbPredicates。它替代了 Android 上的ContentResolver查询参数。orderByDesc(update_time)保证最新修改的笔记排最上面。查询只取主键、标题、更新时间三列内容字段不查列表滚动起来会快很多。页面跳转回传值时工程包用的是router.back结合页面间数据传递但更稳妥的是在onPageShow里重新加载列表。我一般会写一个reload()方法编辑页保存后调用this.router.back({ url: pages/Index })然后首页在onPageShow中调用loadNotes()。这样不管从哪里返回首页数据都是最新的。3.3 新增、编辑、删除的完整操作链路新增和编辑区别不大都是构建ValuesBucket后执行insert或updateasync saveNote(note: Note, isEdit: boolean) { let store await getStore(this.context); let values: data_rdb.ValuesBucket { title: note.title, content: note.content, update_time: Date.now() }; if (!isEdit) { values[create_time] Date.now(); await store.insert(note, values); } else { let predicates new data_rdb.RdbPredicates(note); predicates.equalTo(id, note.id); await store.update(values, predicates); } this.loadNotes(); }删除操作要注意RdbPredicates的equalTo只接受字符串。如果你的 id 是从resultSet.getLong拿的要转成字符串传入否则会查不到记录async deleteNote(id: number) { let store await getStore(this.context); let predicates new data_rdb.RdbPredicates(note); predicates.equalTo(id, id.toString()); // 注意转字符串 await store.delete(predicates); }操作完成后不需要手动关闭resultSet吗需要。工程包在查询结束后会调用resultSet.close()。如果忘了关多次查询后会报 “ResultSet already closed” 或内存增长。建议把查询封装成一个统一方法在 finally 里关掉resultSet避免每个页面都写一遍。这里的参数选择有一个细节insert和update都要求ValuesBucket的 key 必须和表字段完全一致少了create_time插入时数据库会报 NOT NULL 约束失败。所以新增和编辑拆开写比共用一个方法更清晰。4. 适配 HarmonyOS 3 的常见坑从 API 检查到权限声明这一章是工程包最值钱的部分因为没有实际跑到真机上你根本不知道哪些文档没写清楚的坑有多疼。我整理了 4 个高频问题每个都按现象、原因、解决的顺序讲。4.1 页面跳转后数据不刷新生命周期回调没触发现象从编辑页保存后返回首页列表还是旧数据只有再次下拉或重新进入才能看到新笔记。原因HomePage 用aboutToAppear加载数据但aboutToAppear只在页面首次创建时执行一次。从编辑页返回时首页实例并没有销毁重建所以不会触发aboutToAppear。解决把数据加载移到onPageShow里。Index页面模板的onPageShow生命周期在每次从其它页面返回时都会回调。在Index.ets中实现onPageShow() { this.loadNotes(); }onPageShow还有一个特点是在应用从后台切回前台时也会触发所以如果你对加载耗时敏感可以在里面加一个时间判断距离上次加载超过 1 秒才重新查询否则直接用缓存。工程包采用简单方案每次返回都全量加载因为笔记数量一般不大。4.2 数据库版本升级或表结构变更导致应用崩溃现象应用更新后老用户打开应用直接闪退日志里出现RdbStore initial failed。原因getRdbStore的配置里没有指定版本号或者建表语句用CREATE TABLE IF NOT EXISTS只是新建没有处理老表结构变化。当你给note表新增一个字段时老库里的表结构还是旧的但查询语句里用了新字段运行到getColumnIndex时返回 -1导致类型转换异常。解决工程包在getRdbStore的配置中显式声明版本并通过onUpgrade迁移表结构const STORE_CONFIG: data_rdb.StoreConfig { name: note.db, securityLevel: data_rdb.SecurityLevel.S1, version: 2 };然后在getRdbStore的第二个参数放回调data_rdb.getRdbStore(context, STORE_CONFIG, { onUpgrade: (oldVersion, newVersion) { if (oldVersion 2) { store.executeSql(ALTER TABLE note ADD COLUMN remind_time INTEGER); } } });这里的关键是版本号必须递增且onUpgrade里写清楚从旧版本迁移到什么新版本。我在某次改造中用了一个笨办法直接删表重建结果用户数据全没了后来改成先备份旧表数据再迁移。工程包没有这么做因为对记事本来说数据丢了就是事故。4.3 权限弹窗不出现或点完“拒绝”后功能静默失效现象在真机上测试第一次启动时没有弹出权限请求进入编辑页也无法保存图片如果工程包扩展了图片附件功能。原因HarmonyOS 3 的权限模型区分 “用户授权类型” 和 “系统授权类型”。如果module.json5里声明了权限但是在代码里没有调用requestPermissionsFromUser权限弹窗就不会出现。另一个常见错误是把权限声明写在abilities内部而不是module.json5的requestPermissions数组里。解决确认权限声明位置正确{ module: { requestPermissions: [ { name: ohos.permission.WRITE_IMAGEVIDEO, reason: 需要存储笔记图片, usedScene: { abilities: [EntryAbility] } } ] } }然后代码中动态请求import abilityAccessCtrl from ohos.abilityAccessCtrl; async function requestPermission(context: Context): Promiseboolean { const atManager abilityAccessCtrl.createAtManager(); const result await atManager.requestPermissionsFromUser(context, [ohos.permission.WRITE_IMAGEVIDEO]); return result.authResults[0] 0; }注意如果用户第一次点了“拒绝”下次再请求时需要引导到设置页否则requestPermissionsFromUser会直接返回拒绝。工程包里对这种情况做了降级处理没有权限时只保存文本不保存图片附件这样至少不会让应用崩溃。4.4 深色模式下文字看不清资源限定符没有生效现象手机切到深色模式后记事本首页标题仍然是深色文字背景是深色完全看不清。原因HarmonyOS 3 的资源限定符不是自动匹配的。如果你在resources/base/element/下只放了一套颜色那所有模式都用它。需要建立resources/dark/element/目录把深色模式下的颜色覆盖值放进去。解决在工程包中新增深色资源目录entry/src/main/resources/dark/element/colors.json内容示例{ color: [ { name: text_primary, value: #E0E0E0 }, { name: list_background, value: #121212 } ] }然后在页面中使用资源名Text(this.note.title) .fontColor($r(app.color.text_primary))这里最容易忽略的是状态栏和导航栏的颜色也需要在dark目录下单独声明一次。工程包给出的规则是所有在base/element中定义的颜色都要在dark/element中重新定义哪怕你觉得和浅色一样否则深色模式下颜色会来自默认主题出现不确定的色块。5. 性能与体验调优让记事本在低内存设备上也不卡工程包虽然只是个记事本但在 HarmonyOS 3 设备上跑不同机型的表现差异非常大。尤其是中低端设备列表滚动掉帧、冷启动慢、编辑页内容一多就卡顿。以下调优是工程包默认没有做到、但你接手后应该加上的。5.1 列表懒加载与分批渲染首页列表直接用List组件但默认行为是一次性创建全部列表项。当笔记数量涨到几百条ListItem的滚动性能明显下降。工程包的做法是不用ForEach一次性渲染所有数据而是用LazyForEach。实现一个IDataSourceimport { List, LazyForEach, IDatasource } from ohos.arkui; class NoteDataSource implements IDatasource { private list: Note[] []; totalCount(): number { return this.list.length; } getData(index: number): Note { return this.list[index]; } registerDataChangeListener(listener: DataChangeListener): void { this.listener listener; } unregisterDataChangeListener(): void { this.listener undefined; } }LazyForEach只渲染可视区域附近的项滚动时动态回收和创建内存占用稳定在几十 KB 级别。如果你把工程包里的ForEach替换成LazyForEach还需要注意一点数据更新后必须调用listener.notifyDataChanged()否则列表不会感知到新增或删除。很多人在替换后遇到列表不刷新就是这个监听没接好。5.2 编辑页防抖保存与草稿恢复编辑页最怕手滑退出导致内容丢失。工程包在编辑页做了一件事每次输入停止 1 秒后自动保存草稿到内存并在页面销毁前把未保存内容写入一个单独的draft表。这样即使应用被杀重新打开时也能恢复。核心逻辑let timer: number | undefined undefined; onContentChange(text: string) { if (timer) { clearTimeout(timer); } timer setTimeout(() { this.saveDraft(text); }, 1000); }clearTimeout的入参在 ArkTS 中要求是 number需要声明let timer: number | undefined。这个函数如果写在页面里页面销毁时记得clearTimeout(timer)否则定时器回调里访问已经销毁的上下文会报空指针。工程包把草稿存取放在数据源单例中不依赖页面实例这在 HarmonyOS 3 的 4.0 及以后版本配合StateManagementV2比较好用。5.3 字体缩放与横屏切换适配鸿蒙系统允许用户在设置中调整字体大小。如果你的页面用了固定像素值字体会溢出或者被截断。工程包在编辑页文本输入框上用fontSize(16fp)其中fp是鸿蒙特有的字体像素单位随用户字体缩放自适应。注意别和vp混淆vp是虚拟像素不随字体缩放。对于横屏切换工程包的EditPage采用相对布局编辑区占满剩余空间。需要特别注意的是键盘弹起时页面是否 - resize。HarmonyOS 3 默认不是全屏调整需要设置windowStage的键盘模式window.getLastWindow(context).then((win) { win.setWindowLayoutFullScreen(true); win.setWindowSystemBarProperties({ isStatusBarVisible: true }); });如果这里不设置键盘会盖住输入框导致录入体验很差。工程包默认给了adjustResize风格的实现但只对 API 10 以上生效API 9 设备上建议用setWindowLimits控制最大高度。6. 进阶技巧用工程包改造成待办事项应用记事本工程包最实用的扩展方向就是待办事项。你要做的不是推翻重来而是在已有数据层上小心地加字段和状态。我建议按下面的顺序操作每一步都能用真机验证。第一步给note表加两个字段is_done和remind_time。不要用布尔值存是否完成用 INTEGER 存 0/1因为 SQLite 对布尔支持不好。数据库版本号从 2 升到 3并在onUpgrade中迁移onUpgrade: (oldVersion, newVersion) { if (oldVersion 3) { store.executeSql(ALTER TABLE note ADD COLUMN is_done INTEGER DEFAULT 0); store.executeSql(ALTER TABLE note ADD COLUMN remind_time INTEGER DEFAULT 0); } }第二步改造首页列表项。让每一行左侧显示一个勾选圆圈右侧是标题和更新时间。点击圆圈切换is_done点击文字进入编辑页。切换逻辑复用之前的update方法只更新is_done字段async toggleDone(id: number, current: number) { let store await getStore(this.context); let values: data_rdb.ValuesBucket { is_done: current 0 ? 1 : 0, update_time: Date.now() }; let predicates new data_rdb.RdbPredicates(note); predicates.equalTo(id, id.toString()); await store.update(values, predicates); }第三步增加“待办”和“已完成”两个标签页。这里不需要新建页面在首页顶部放Tabs两个 Tab 共用同一个List组件通过切换时重新查询数据来实现。查询时增加predicates.equalTo(is_done, 0)或1注意equalTo的第二个参数是字符串数字类型要转成字符串。第四步加提醒功能。HarmonyOS 3 的提醒可以使用AlarmManager但需要在module.json5中声明ohos.permission.PUBLISH_AGENT_REMINDER。设置提醒时把remind_time存进数据库同时调用系统的提醒接口。这里最容易踩坑的是提醒时间传参remind_time存的是毫秒时间戳但AlarmManager的setAlarm接收的是毫秒值注意别在传参时又乘了一次 1000。最后验证改造是否成功我一般会做三层。第一层写一条笔记后杀掉应用重开确认数据还在。第二层把系统时间调到提醒前 30 秒确认横幅通知弹出。第三层切换深色模式和最大字体确认列表文字不截断、勾选图标和文字对齐正常。这套工程包给我最大的教训是不要过度设计。记事本就是增删改查你只需要把数据层做实、把生命周期理顺、把权限和资源适配到位剩下的事都是复制粘贴再加一点状态字段。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑