2026/10/18 18:12:19

鸿蒙原生记事本开发工程包:Stage模型与ArkTS实战避坑指南

鸿蒙原生记事本开发工程包:Stage模型与ArkTS实战避坑指南 简介这是一套基于ArkTS语言开发的鸿蒙原生记事本应用工程包适配HarmonyOS 3.0及以上版本面向初学鸿蒙应用开发的学生和需要快速定制笔记工具的开发者。工程结构完整包含entry模块、基础UI组件、本地数据存储逻辑以及任务增删改查功能可在DevEco Studio中直接同步并真机调试配套中文说明文档提供环境配置与常见问题处理思路。资源包共124个文件以xml界面布局、java逻辑代码、png图标资源为主另含gradle工程配置、json配置文件等压缩包大小11.6MB目录规范且已预置.gitignore与.idea配置。目前已有36人学习下载。借助该工程包读者能快速熟悉HarmonyOS工程结构、ArkTS编程范式及本地存储实践也可基于干净的框架进行二次开发省去从零搭建的繁琐过程。1. 鸿蒙原生记事本应用开发工程包为什么说“简单应用”反而是最好的上手样本记事本可能是鸿蒙原生开发里最容易被人低估的一个题材。单看功能无非是列表、编辑、保存三条路但真正动手用 HarmonyOS 3 的 Stage 模型从头到尾搭一遍你会发现它几乎覆盖了日常业务应用会用到的全部底层能力ArkTS 的声明式 UI、页面路由与数据持久化、应用生命周期管理、以及 API 版本差异带来的适配问题。这个工程包存在的意义就在于此——拿到它你就等于拿到了一套「在最简单的业务场景下验证鸿蒙开发最佳实践」的参考实现而不是像看官方文档那样每个知识点都是孤立的。适合拿起这个工程包的是两类人一类是从 Android 或前端转过来的开发者想快速搞清楚 Stage 模型和 FA 模型的差异到底落在哪些文件上另一类是已经写过几个 Demo、但始终不确定自己的工程结构是否符合上架规范的新手。前者能借它省掉翻文档的时间后者能借它改掉「能跑就行」的坏习惯。接下来我从工程结构、构建运行、数据持久化到踩坑排查一条线讲完。2. 工程包结构拆解Stage 模型与 ArkTS 代码组织方式2.1 从 FA 到 StageHarmonyOS 3 的工程包为什么这样组织在 HarmonyOS 3 之前应用开发使用的是 FAFeature Ability模型一个页面就是一个 Ability页面之间通过PA或router跳转整个应用没有一个统一的入口上下文。Stage 模型改变了这套组织方式应用被拆成UIAbilityWindowStageAbilityStage三层每个 UIAbility 都持有自己的Context而业务代码不再以 Ability 为单位散落而是集中在ets目录下按页面和组件分层。这个工程包采用的就是 Stage 模型。打开工程根目录你会看到这样的结构这也是拿到手之后第一眼应该确认的东西project_root/ ├── AppScope/ │ ├── app.json5 # 应用全局配置名称、版本、图标 │ └── resources/ # 全局资源存放应用级图标 ├── entry/ │ ├── src/main/ │ │ ├── ets/ │ │ │ ├── entryability/ # 入口 UIAbility │ │ │ ├── pages/ # 页面级 ArkTS 代码 │ │ │ ├── model/ # 数据模型与数据库操作封装 │ │ │ └── common/ # 公共常量与工具方法 │ │ ├── resources/ # 模块级资源字符串、颜色、布局 │ │ └── module.json5 # 模块配置权限、页面声明、入口 │ └── build-profile.json5 # 构建配置签名、目标设备 └── oh-package.json5 # 第三方依赖声明注意ets目录下的分层这是工程包和随手写的 Demo 最大的区别。entryability存放应用入口pages只放页面组件model层单独管理数据库和偏好存储common放常量。这个分层不是摆设后续如果要接入网络请求或者迁移到 API 10你只需要替换model层的实现pages里的界面代码一行都不用动。2.2 module.json5 里的门道入口声明与权限配置很多开发者在 HarmonyOS 3 适配期遇到的第一个报错不是代码问题而是module.json5里漏写了一行配置。这个文件相当于应用的「清单文件」它决定了系统如何找到你的入口 Ability、申请哪些权限、以及设备在什么场景下可以拉起应用。记事本工程包里module.json5的几个关键字段值得照着核对一遍{ module: { name: entry, type: entry, description: $string:module_desc, mainElement: EntryAbility, deviceTypes: [phone, tablet], deliveryWithInstall: true, abilityBackToHome: false, abilities: [ { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ts, description: $string:EntryAbility_desc, icon: $media:icon, label: $string:EntryAbility_label, startWindowIcon: $media:icon, startWindowBackground: $color:start_window_background, exported: true, skills: [ { entities: [entity.system.home], actions: [action.system.home] } ] } ] } }mainElement必须和你abilities数组里的某个 name 对应否则编译报错srcEntry指向的是ets目录下entryability里的.ts文件注意这里不需要写.ets后缀skills里的entity.system.home和action.system.home是应用图标出现在桌面上的必要条件漏掉一个装到真机上会找不到应用入口。我一般会建议拿到工程包先跑一次编译编译过了再改业务代码——这句话在这类工程上能省掉大量排查成本。2.3 EntryAbility 生命周期onCreate 与 onWindowStageCreate 的分工Stage 模型下EntryAbility的生命周期回调比 FA 模型多了一层。FA 时代你在onStart里一股脑做初始化现在则需要区分「应用级初始化」和「页面级初始化」。这个工程包里是这么拆的// entryability/EntryAbility.ts import UIAbility from ohos.app.ability.UIAbility; import window from ohos.window; import { PreferencesHelper } from ../model/PreferencesHelper; export default class EntryAbility extends UIAbility { onCreate(want, launchParam) { // 应用级初始化这里只做全局数据层的准备 // 例如创建数据库连接实例、读取全局配置 console.info(EntryAbility onCreate); } onWindowStageCreate(windowStage: window.WindowStage) { // 页面级初始化windowStage 就绪后加载首页 windowStage.loadContent(pages/Index, (err) { if (err.code) { console.error(Failed to load content: JSON.stringify(err)); return; } console.info(Index page loaded); }); } onDestroy() { // 应用退出前的收尾关闭数据库连接、释放资源 console.info(EntryAbility onDestroy); } }onCreate里的代码要克制千万别把数据库建表、数据迁移这类耗时操作塞进来否则会影响冷启动体验onWindowStageCreate是加载首页的唯一正确位置拿到windowStage之后才能调loadContent。工程包在onCreate里只放了一句日志和注释这个「留白」本身就是一种示范哪些初始化应该做、哪些不应该做边界在新手阶段最容易模糊。3. 把记事本跑起来构建配置、签名与首选项持久化3.1 用 IDE 导入工程包并完成 HarmonyOS 3 适配检查拿到工程包之后第一步不是急着看代码而是确认它能在你本地的开发环境里编译通过。HarmonyOS 3 对应的是 API 9需要你本地安装的 SDK 版本不低于 9同时 IDE 版本也要能支持 Stage 模型工程。导入流程版本差异不大但每一步都有验证点# 1. 确认本机 SDK 版本确保不低于 HarmonyOS 3 对应的 API 9 # 打开 IDE 的 SDK Manager 查看已安装的 SDK 版本 # 2. 用 IDE 直接打开工程根目录 # 注意是打开包含 AppScope 和 entry 的根目录不是 entry 子目录 # 3. 首次打开会触发 Gradle 或 hvigor 同步 # 等待右下角构建进度条走完确认无红色报错 # 4. 配置自动签名 # 在 IDE 的 Project Structure 里选择 Signing Configs # 勾选 Automatically generate signature填入开发者信息导入后常见的适配检查点是build-profile.json5里的compatibleSdkVersion。如果工程包默认写的是compatibleSdkVersion: 9而你本机只装了 API 8 的 SDK编译时会直接提示 SDK 版本不匹配。反过来如果你本机已经装到 API 10 或更高也建议先保持 API 9 的兼容声明不动而不是一上来就升级改造——先跑通再谈适配新版本是这类工程包最稳妥的打开方式。3.2 数据持久化两条路关系型数据库与首选项为什么选了后者记事本应用的核心数据只有「笔记列表 单条笔记内容」这类轻量数据在 HarmonyOS 3 下有两条常规存储路径关系型数据库RDB和首选项Preferences。工程包选的是首选项原因是它把「读-改-写」整个流程压缩成了三个同步方法代码量只有 RDB 方案的零头。// model/PreferencesHelper.ts import dataPreferences from ohos.data.preferences; import common from ohos.app.ability.common; const STORE_NAME notes_store; const KEY_NOTES notes_list; export class PreferencesHelper { private pref: dataPreferences.Preferences | null null; // 初始化拿到应用的 Context然后加载偏好存储实例 async init(context: common.UIAbilityContext) { this.pref await dataPreferences.getPreferences(context, STORE_NAME); console.info(Preferences initialized); } // 读取所有笔记JSON 字符串反序列化成数组 async getNotes(): Promiseany[] { if (!this.pref) return []; const jsonStr await this.pref.get(KEY_NOTES, []); return JSON.parse(jsonStr as string); } // 保存笔记列表整个数组序列化成 JSON 写入 async saveNotes(notes: any[]) { if (!this.pref) return; await this.pref.put(KEY_NOTES, JSON.stringify(notes)); await this.pref.flush(); } }init方法必须传入UIAbilityContext这个对象是从EntryAbility的onCreate参数里拿到的或者通过页面getContext()获取flush()是落盘的关键只put不flush数据在应用退出后会丢这是首选项方案最容易翻车的地方。整个实现只有三十行代码却完整覆盖了读、写、持久化三个能力。3.3 列表页与编辑页ArkUI 声明式语法的最小闭环页面部分工程包使用了 ArkUI 的声明式写法。列表页用List组件渲染笔记标题编辑页用TextArea接收输入两者通过router进行跳转并传递笔记 ID。这形成了一个「列表 → 编辑 → 返回 → 列表刷新」的最小闭环// pages/Index.ets — 列表页核心片段 import router from ohos.router; import { PreferencesHelper } from ../model/PreferencesHelper; Entry Component struct Index { State notes: any[] []; private prefHelper new PreferencesHelper(); aboutToAppear() { this.loadNotes(); } async loadNotes() { const context this.getContext(this) as any; await this.prefHelper.init(context); this.notes await this.prefHelper.getNotes(); } build() { Column() { List({ space: 12 }) { ForEach(this.notes, (note: any, index: number) { ListItem() { Row() { Text(note.title) .fontSize(16) .margin({ left: 16 }) Blank() Text(note.time || ) .fontSize(12) .fontColor(#999999) .margin({ right: 16 }) } .width(100%) .height(56) .backgroundColor(#FFFFFF) .borderRadius(8) } .onClick(() { router.pushUrl({ url: pages/Edit, params: { noteId: index } }); }) }, (note: any, index: number) index.toString()) } .layoutWeight(1) .width(100%) .padding(12) Button(新增笔记) .onClick(() { router.pushUrl({ url: pages/Edit }); }) } .width(100%) .height(100%) .backgroundColor(#F1F3F5) } }State是 ArkUI 响应式系统的核心当this.notes被重新赋值时界面自动刷新不需要手动调用任何刷新方法ForEach的第三个参数是键值生成器这里用索引作为 key是为了避免数组项被删除后 key 重复导致渲染混乱。页面的跳转用router.pushUrlHarmonyOS 3 下这是最直接的页面导航方式。编辑页保存后返回列表页列表页依靠aboutToAppear里重新执行loadNotes来感知数据变化这个机制在下一章的避坑环节会再次提到。4. 记事本开发避坑指南HarmonyOS 3 适配的 4 个典型问题4.1 坑一API 9 下首选项包名报错编译找不到模块现象在 API 9 的工程里写import dataPreferences from ohos.data.preferences编译时提示找不到模块ohos.data.preferences或者运行时直接抛Cannot find module异常。原因HarmonyOS 3 早期版本API 8 到 API 9 的过渡期对该模块的包名做了调整部分 SDK 版本中模块名是ohos.data.preference没有末尾的 s而 API 9 稳定版统一为ohos.data.preferences。工程包如果是在某个中间版本 SDK 上生成的导入到你本地后就会出现这个错位。解决打开工程根目录确认 SDK 版本与build-profile.json5的compatibleSdkVersion是否匹配。如果本地 SDK 是 API 9把导入语句统一改成ohos.data.preferences如果是 API 8 的 SDK则需要改回ohos.data.preference。另外检查oh-package.json5里是否声明了对应的依赖组件该模块属于系统 SDK 内置不应出现在第三方依赖中。4.2 坑二模拟器上数据能存进去重启后全部消失现象在模拟器里新增几条笔记杀进程重进之后数据还在但模拟器冷重启后列表恢复成空白。真机上暂时没复现。原因flush()只是把偏好数据从内存写到了应用私有目录下的文件但如果模拟器的持久化分区没有被正确挂载或者你在测试时勾选了「清除应用数据」选项私有目录会随应用卸载或数据清除一起被抹掉。另一个隐蔽原因是pref.flush()是异步方法如果保存后立刻杀进程写入可能还没真正落盘。解决保存笔记后在回调里确认写入完成再做页面关闭或跳转同时检查模拟器的存储设置确认「持久化存储」是开启状态。如果是通过命令行启动的模拟器注意不要加-no-snapshot之类的参数否则每次冷启动都会还原到初始镜像状态。真机上优先测试模拟器只做功能验证。4.3 坑三列表页返回后不刷新新增的笔记看不到现象从编辑页保存笔记返回列表页列表依旧是旧数据。手动下拉或者杀掉重进才能看到新内容。原因aboutToAppear只在页面初次创建时触发一次而通过router.pushUrl跳转再返回时原页面并没有被销毁而是回到了前台。此时aboutToAppear不会再次执行除非你在跳转时设置了页面栈的销毁逻辑或者编辑页通过回调通知列表页。解决在onPageShow生命周期里重新加载数据这是页面每次从后台回到前台都会触发的回调。把loadNotes()从aboutToAppear移到onPageShow既保留首次加载能力又覆盖返回刷新的场景onPageShow() { this.loadNotes(); }这个方法比在编辑页手动调用router.back({ uri: ... })并传递回调参数要可靠得多。尤其是当你后续给记事本加了编辑和删除功能后onPageShow方案能保证所有改动在返回那一瞬间全部同步到列表。4.4 坑四List 嵌套滚动卡顿滑动掉帧明显现象笔记数量超过 50 条后列表滑动开始掉帧快速滑动时甚至出现白屏闪动。查看日志无任何报错。原因ForEach渲染的列表项里有Blank()组件和大量属性链式调用而 ArkUI 在重绘时会为每一个组件执行完整的样式解析。另一个常见原因是ListItem没有被LazyForEach接管所有数据项一次性全量渲染数据量上来后每帧的构建工作量暴增。解决有两种做法通常情况下先做第一种。第一种是把List的cachedCount调小控制预渲染数量第二种是数据量较大时改用LazyForEach让列表按需渲染List({ space: 12 }) { LazyForEach(this.notes, (note: any, index: number) { ListItem() { // 列表项内容不变 } }, (note: any, index: number) index.toString()) } .cachedCount(5)另外检查列表项的.onClick回调里是否有高频闭包创建。在构建函数里直接写箭头函数每次渲染都会生成新的函数实例建议把点击逻辑抽成独立方法传入。首选项方案存 50 条笔记的性能压力其实不大真正卡顿的原因几乎都在渲染层。5. 从工程包到自有应用改造方向、验证方法与一个值得养成的习惯工程包跑通之后下一步不是急着加功能而是先做一次「可信度验证」——确认它不只是在模拟器上能跑在真机上也能稳定工作。两个验证动作第一安装到 HarmonyOS 3 真机上连续新增 30 条笔记每条写满一屏文字再逐条打开检查内容完整性第二杀掉应用进程后重启确认所有笔记和编辑时间都在。这一步能同时暴露首选项写入时序、列表渲染、生命周期回调三个层面的潜在问题。验证通过后改造方向可以按三条线展开。第一条线是增加搜索能力在model层加一个按关键字过滤笔记的方法页面加一个Search组件这能让你真正理解「数据层和界面层分层」带来的收益。第二条线是给笔记增加分类标签这是从「一个数组存全部」走向「按维度组织数据」的经典一步你会开始接触多表或者多 key 的存储设计。第三条线是迁移到 API 10 或更高版本把首选项换成关系型数据库方案这一步不是必须但能帮你对比出两种持久化方案的边界在哪里笔记量超过几百条后首选项的 JSON 全量读写方案会让每次保存耗时明显上升。最后说一个值得长期保留的习惯每次改动工程包里的model层代码顺手在文件顶部写一行注释记录改动原因和对应 API 版本。HarmonyOS 3 这套生态的 API 演进速度不慢很多包名和行为的差异只在特定版本区间存在。等你三个月后回头改一段代码如果注释里写着「API 9 下必须使用 preferences 带 s 的版本API 10 改回 preference」你就能省下至少半天的时间去重新踩一遍同样的坑。记事本这个工程包的真正价值不在代码量而在于它用最小的体量把这些版本差异的边界清晰地标了出来——你在这个小项目里建立起来的排查思路后面迁移到任何鸿蒙原生应用里都不会浪费。希望这篇笔记能帮你少走一段弯路。本文还有配套的精品资源点击获取