鸿蒙原生笔记应用开发实战 — 从零搭建到运行
一、前言
在前几篇文章中,我们先后完成了鸿蒙计算器和待办事项应用的开发,掌握了 ArkUI 的页面布局、状态管理、列表渲染和数据持久化等核心技能。今天我们将这些知识综合运用,开发一个功能更完整的项目——笔记应用(Notes App)。
笔记应用是移动端最典型的工具类应用之一。无论是系统自带的备忘录,还是第三方的 Notion、有道云笔记,其核心功能都是新建、编辑、保存和删除笔记。和单纯的待办应用相比,笔记应用需要处理更复杂的场景:多页面之间的跳转与参数传递、新建与编辑复用同一个页面、跨页面的数据同步、以及更丰富的内容输入方式。这些恰恰是实际鸿蒙应用开发中最常见、最核心的需求模式。
本文将从项目创建开始,带你一步步实现一个功能完整的鸿蒙笔记应用。与之前文章的"一步到位"风格不同,本文将更注重设计思路的讲解——为什么要这样组织代码?为什么需要两个生命周期函数配合?跨页面数据是怎么同步的?理解了这些设计决策,你才能在遇到类似需求时举一反三。
二、项目准备
2.1 开发环境要求
- IDE:DevEco Studio(最新版本,可从华为开发者官网下载)
- SDK:API 23 或以上,通过 SDK Manager 确认已安装
- 语言/框架:ArkTS + ArkUI
- 模拟器:Phone_API23(1080 × 1920 分辨率)
2.2 新建项目
打开 DevEco Studio,点击 File → New → Create Project,选择 Empty Ability 模板,填写以下项目信息:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 项目名称 | NotesApp | 使用英文,不要出现中文 |
| 包名 | com.example.notesapp | 应用唯一标识,反向域名格式 |
| 保存路径 | D:\Projects\NotesApp | 必须为纯英文路径 |
| Compile SDK | API 23 | 根据已安装的 SDK 版本选择 |
| 模型 | Stage | 官方推荐的应用开发模型 |
| 模块名称 | entry | 应用入口模块,保持默认 |
点击 Finish 完成创建。IDE 会自动同步项目配置,首次同步通常需要 1-3 分钟。同步完成后,可以看到标准的 HarmonyOS 项目目录结构。
三、功能设计
3.1 双页面架构
笔记应用采用双页面架构:
- 列表页(Index.ets):应用首页,以卡片列表形式展示所有笔记。每张卡片显示标题、内容摘要和修改时间。顶部有新建按钮。
- 详情页(NoteDetail.ets):笔记的新建和编辑都在此页面完成。通过路由参数区分新建(-1)和编辑(实际 ID)两种模式。
两个页面通过路由跳转连接,通过全局存储 @StorageLink 共享数据。
3.2 界面布局
列表页布局:
┌─────────────────────────────┐
│ 📝 我的笔记 ➕ 新建 │ ← 顶部标题栏
├─────────────────────────────┤
│ ┌─────────────────────┐ │
│ │ 会议记录 🗑️ │ │ ← 笔记卡片(标题 + 摘要 + 时间)
│ │ 今天下午三点讨... │ │
│ │ 05-31 14:30 │ │
│ ├─────────────────────┤ │
│ │ 购物清单 🗑️ │ │
│ │ 牛奶、鸡蛋、... │ │
│ │ 05-30 10:15 │ │
│ └─────────────────────┘ │
└─────────────────────────────┘
详情页布局:
┌─────────────────────────────┐
│ ← 返回 💾 保存 │ ← 顶部导航栏
├─────────────────────────────┤
│ 输入标题... │ ← 单行标题输入
├─────────────────────────────┤
│ │
│ 开始记录... │ ← 多行内容输入
│ │
│ │
└─────────────────────────────┘
3.3 数据模型
笔记的数据结构如下:
interface Note {
id: number // 唯一标识,用于精确查找和删除
title: string // 笔记标题,可为空字符串
content: string // 笔记正文内容
timestamp: number // 创建或最后修改时间的时间戳
}
四个字段各司其职:id 是数据库层面的主键,用于区分和定位每一条笔记;title 和 content 是用户数据的主体;timestamp 用于列表按时间排序,并在卡片上显示可读的日期时间。
3.4 功能清单
| 功能 | 详细说明 | 实现位置 |
|---|---|---|
| 查看笔记列表 | 按时间倒序显示所有笔记卡片 | Index.ets → build() |
| 内容摘要 | 显示内容的前 40 个字符 | Index.ets → getPreview() |
| 时间格式化 | 将时间戳转为 MM-DD HH:MM 格式 | Index.ets → formatDate() |
| 新建笔记 | 跳转到详情页,传入 ID = -1 | Index.ets → openNewNote() |
| 编辑笔记 | 点击卡片跳转,传入实际 ID | Index.ets → openNote() |
| 保存笔记 | 新建或修改后写回存储并返回 | NoteDetail.ets → saveNote() |
| 删除笔记 | 点击 🗑️ 按钮移除 | Index.ets → deleteNote() |
| 数据持久化 | @StorageLink 自动保存到本地 | 两个页面共享 |
| 列表自动刷新 | 返回时通过 onPageShow 重新加载 | Index.ets → onPageShow() |
| 空状态提示 | 无笔记时显示引导文字 | Index.ets → build() |
四、完整代码实现
在开始写代码之前,先明确一下 ArkTS 的一些语法约束。与标准 TypeScript 不同,ArkTS 为了运行时性能和安全考虑,做了一些语法限制:不允许使用数组展开运算符(...arr),不允许使用 map、filter、reduce 等高阶函数,不允许使用对象展开({...obj})。我们在笔记应用中将严格遵守这些约束,所有数组操作都使用 concat 或 for 循环实现。
五、核心技术实现
笔记应用涉及两个页面之间的协同工作,核心逻辑集中在三个方面:页面路由与参数传递、数据持久化的跨页面同步、以及笔记的新建与编辑复用。
4.1 页面路由配置
笔记应用包含两个页面,需要在路由配置文件中注册。打开 entry/src/main/resources/base/profile/main_pages.json:
{
"src": [
"pages/Index",
"pages/NoteDetail"
]
}
这是多页面开发中最容易被忽略的一步。如果只写了 Index 而没有注册 NoteDetail,IDE 在编译时不会报错,但运行时跳转会失败并显示白屏。每次新增页面,都必须在这里注册。
4.2 状态管理与持久化
笔记应用使用两种装饰器管理数据:
@State:驱动 UI 渲染。notes 数组是列表页的核心数据源,任何增删改操作后重新赋值都会触发 UI 自动更新。
@StorageLink:实现数据持久化。它绑定到键名 'notes_data',变量赋值时自动写入设备本地存储,应用启动时自动读取恢复。
两个页面的配合方式:
// Index.ets
@State notes: Note[] = []
@StorageLink('notes_data') savedNotes: string = '[]'
// NoteDetail.ets
@StorageLink('notes_data') savedNotes: string = '[]'
关键设计要点是两个页面的 @StorageLink 使用了完全相同的键名 'notes_data'。这意味着它们操作的是同一个底层存储——NoteDetail 写入数据后,Index 能立即通过 loadNotes() 读取到最新内容。
4.3 页面跳转与参数传递
从列表页到详情页的跳转使用 router.pushUrl:
private openNewNote(): void {
router.pushUrl({
url: 'pages/NoteDetail',
params: { noteId: -1 } // -1 表示新建
})
}
private openNote(id: number): void {
router.pushUrl({
url: 'pages/NoteDetail',
params: { noteId: id } // 实际 ID 表示编辑
})
}
在详情页中接收参数:
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>
if (params) {
this.noteId = params['noteId'] as number
}
if (this.noteId !== -1) {
this.loadNote() // 编辑模式:加载已有内容到输入框
}
// ID 为 -1 时保持空白输入框,等待用户输入
}
通过 noteId = -1 这个约定值来区分新建和编辑,避免了为这两种模式分别编写两个页面。当 noteId 为 -1 时详情页保持空白,等待用户输入;当 noteId 为有效值时,自动从存储中查找并加载对应的笔记内容到输入框。
4.4 笔记保存流程
保存是笔记应用最核心的操作,涉及数据读取、新建/编辑判断、写入存储和页面返回四个步骤:
private saveNote(): void {
// 第一步:从 @StorageLink 读取已有数据
let notes: Note[] = []
if (this.savedNotes && this.savedNotes !== '[]') {
notes = JSON.parse(this.savedNotes)
}
// 第二步:判断是新建还是编辑
if (this.noteId === -1) {
// 新建:生成新 ID
let maxId = 0
for (let i = 0; i < notes.length; i++) {
if (notes[i].id > maxId) maxId = notes[i].id
}
const newNote: Note = {
id: maxId + 1,
title: this.titleText,
content: this.contentText,
timestamp: Date.now()
}
notes = [newNote].concat(notes)
} else {
// 编辑:遍历查找并替换
const newNotes: Note[] = []
for (let i = 0; i < notes.length; i++) {
if (notes[i].id === this.noteId) {
newNotes.push({
id: notes[i].id,
title: this.titleText,
content: this.contentText,
timestamp: Date.now()
})
} else {
newNotes.push(notes[i])
}
}
notes = newNotes
}
// 第三步:写回持久化存储
this.savedNotes = JSON.stringify(notes)
// 第四步:返回列表页
router.back()
}
保存完成后 router.back() 返回列表页。此时列表页的 onPageShow() 生命周期触发,从 @StorageLink 重新加载数据,界面自动刷新。
完整的保存数据流如下:
用户填写内容 → 点击保存
→ 解析 @StorageLink 中的 JSON 字符串为数组
→ 如果是新建:生成自增 ID,插入数组最前面
→ 如果是编辑:遍历数组,替换匹配 ID 的笔记
→ 将数组序列化为 JSON 写回 @StorageLink
→ router.back() 返回列表页
→ 列表页 onPageShow() 触发
→ loadNotes() 从 @StorageLink 读取最新数据
→ 赋值给 @State notes → UI 自动更新
4.5 两个生命周期函数的配合
列表页使用了两个生命周期函数:
aboutToAppear(): void {
this.loadNotes() // 页面首次创建时加载
}
onPageShow(): void {
this.loadNotes() // 每次页面变为可见时加载
}
两者的区别在于触发时机:aboutToAppear 只在页面实例化时执行一次,适合做一次性初始化;onPageShow 每次页面显示到前台时都会执行,包括从其他页面返回时。如果没有 onPageShow,用户从详情页保存返回后,列表页不会刷新,新添加的笔记不会显示。
4.6 UI 构建详解
列表页使用 List + ListItem 实现高效的滚动列表:
List({ space: 10 }) {
ForEach(this.sortedNotes, (note: Note) => {
ListItem() {
Row() {
Column() {
Text(note.title.length > 0 ? note.title : '无标题')
.fontSize(18).fontWeight(FontWeight.Medium).fontColor(Color.White)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.getPreview(note.content))
.fontSize(14).fontColor('#8E8E93')
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.formatDate(note.timestamp))
.fontSize(12).fontColor('#555555')
}
.layoutWeight(1)
Button('🗑️')
.fontSize(18).width(40).height(40)
.backgroundColor(Color.Transparent)
.onClick(() => { this.deleteNote(note.id) })
}
.backgroundColor('#1C1C1E').borderRadius(12)
.onClick(() => { this.openNote(note.id) })
}
}, (note: Note) => note.id.toString())
}
每张卡片显示三个信息层级:标题(18 号加粗白色)、内容摘要(14 号灰色,限制 40 字)、时间戳(12 号浅灰色),形成清晰的视觉层次。textOverflow 配合 maxLines(1) 实现单行省略号效果,防止长文本破坏布局。
详情页使用 TextInput 和 TextArea 分别处理标题和正文:
TextInput({ placeholder: '输入标题...', text: this.titleText })
.fontSize(22).fontWeight(FontWeight.Bold)
.fontColor(Color.White).placeholderColor('#555555')
.backgroundColor(Color.Transparent).height(56)
.onChange((value: string) => { this.titleText = value })
Divider().height(1).color('#333333').width('92%')
TextArea({ placeholder: '开始记录...', text: this.contentText })
.fontSize(16).fontColor(Color.White).placeholderColor('#555555')
.backgroundColor(Color.Transparent).layoutWeight(1)
.onChange((value: string) => { this.contentText = value })
TextInput 适合单行输入(标题),TextArea 支持多行文本且通过 layoutWeight(1) 自动占满剩余空间,适合笔记正文的书写。
4.7 删除与排序
删除操作和待办应用一样,使用不可变更新模式:
private deleteNote(id: number): void {
const newNotes: Note[] = []
for (let i = 0; i < this.notes.length; i++) {
if (this.notes[i].id !== id) {
newNotes.push(this.notes[i])
}
}
this.notes = newNotes
this.saveNotes()
}
列表排序通过 getter 属性实现按时间倒序:
get sortedNotes(): Note[] {
const result: Note[] = []
for (let i = this.notes.length - 1; i >= 0; i--) {
result.push(this.notes[i])
}
return result
}
五、运行与测试
5.1 文件操作
在 DevEco Studio 中完成以下三步:
- 替换 Index.ets:打开
entry/src/main/ets/pages/Index.ets,全选替换为列表页代码 - 新建 NoteDetail.ets:在
pages目录上右键 → New → File,命名为NoteDetail.ets,粘贴详情页代码 - 更新路由:打开
resources/base/profile/main_pages.json,确保注册了两个页面
5.2 功能测试
| 编号 | 测试操作 | 预期结果 |
|---|---|---|
| 1 | 首次启动应用 | 显示空状态引导:“还没有笔记,点右上角新建” |
| 2 | 点击右上角"新建" | 跳转到详情页,标题和内容均为空 |
| 3 | 输入标题"会议记录"和内容 → 点保存 | 返回列表,显示新笔记卡片 |
| 4 | 点击笔记卡片 | 跳转到详情页,标题和内容已填充 |
| 5 | 修改内容 → 点保存 | 返回列表,内容已更新 |
| 6 | 点 🗑️ 删除按钮 | 笔记从列表中消失 |
| 7 | 连续添加 5 条笔记 | 按时间倒序排列,最新在最上方 |
| 8 | 退出应用重新打开 | 所有笔记数据依然存在 |
5.3 常见编译问题
在编译笔记应用时,有以下几个容易踩坑的地方:
问题 1:找不到页面
如果运行时提示页面不存在,检查 main_pages.json 是否注册了 NoteDetail。这是多页面开发最常见的错误——IDE 不会在编译时检查页面是否存在,只有在运行时跳转才会报错。
问题 2:数据不刷新
如果保存后返回列表页发现数据没有更新,检查 Index.ets 中是否有 onPageShow() 方法。如果只用了 aboutToAppear(),返回时不会触发数据重新加载。
问题 3:@StorageLink 键名不一致
两个页面使用 @StorageLink 时必须使用完全相同的键名。一个写 'notes_data',另一个写 'notesData',会导致数据无法同步。建议将键名定义为常量字符串,两处保持一致。
六、运行效果


七、与其他项目的对比
将笔记应用和之前开发的计算器、待办应用做横向对比,可以看出鸿蒙开发技能的递进关系:
| 对比维度 | 计算器 | 待办应用 | 笔记应用 |
|---|---|---|---|
| 页面数量 | 1 个 | 1 个 | 2 个 |
| 路由跳转 | 无 | 无 | pushUrl + back |
| 参数传递 | 无 | 无 | router.getParams |
| 跨页面数据 | 无 | 无 | @StorageLink 共享 |
| 生命周期 | 无特殊需求 | aboutToAppear | aboutToAppear + onPageShow |
| 输入组件 | Button 点击 | TextInput | TextInput + TextArea |
| 核心复杂度 | 运算逻辑 | 增删改查 | 多页面协作 |
从表格可以清楚看到,每一个项目都在前一项目的基础上引入了新的知识点。如果你顺序完成了这三个项目,你接触到的 ArkUI 知识已经覆盖了:
- 基础组件:Text、Button、TextInput、TextArea、List、Divider
- 状态管理:@State、@StorageLink
- 生命周期:aboutToAppear、onPageShow、aboutToDisappear
- 路由:pushUrl、back、getParams
- 数据处理:JSON 序列化、不可变数组更新、时间格式化
这些知识足够支撑你开发大部分工具类鸿蒙应用。
八、扩展思路
- 搜索功能:在列表页顶部增加搜索框,使用
includes方法根据标题或内容实时筛选笔记 - 富文本编辑:引入 RichEditor 组件,支持加粗、列表、图片插入等富文本格式
- 分类标签:为笔记增加
category字段,支持按标签分类筛选 - 深浅主题:参考计算器扩展篇,增加深色/浅色主题切换功能
- 回收站机制:删除的笔记先标记为
deleted而非直接移除,支持 30 天内恢复
九、总结
本文从零开始完成了鸿蒙笔记应用的开发,这是继计算器和待办应用之后的第三个完整项目。与之前两个项目不同,笔记应用的核心挑战不在于复杂的运算逻辑或精细的 UI 设计,而在于多页面之间的数据协作。两个页面如何共享数据?返回时如何自动刷新?新建和编辑如何复用同一个页面?这些问题的解决方案,构成了本文最值得关注的内容。
核心知识点对照如下:
| 知识点 | 具体应用 | 与待办 App 的差异 |
|---|---|---|
| 多页面路由 | router.pushUrl 跳转 + router.back 返回 |
待办 App 只有单页面 |
| 参数传递 | params 传递 noteId,区分新建与编辑 |
新增知识点 |
| 跨页面数据同步 | 两个页面共享同一个 @StorageLink 键名 |
新增知识点 |
| 双生命周期 | aboutToAppear + onPageShow 配合 |
新增 onPageShow |
| 文本输入 | TextInput 单行 + TextArea 多行 |
待办只有 TextInput |
| 不可变更新 | concat + for 循环构建新数组 |
与待办一致 |
从计算器到待办应用,再到笔记应用,ArkUI 的知识体系逐步深入。计算器侧重单一页面的交互逻辑,待办应用引入列表和数据持久化,笔记应用则进一步扩展到多页面协作、路由跳转和参数传递。掌握了这三个项目,你已经具备了独立开发鸿蒙工具类应用的基础能力。后续可以尝试挑战更复杂的项目——结合网络请求的天气应用、使用 Canvas 绘图的绘图板、或接入华为帐号服务的登录应用。鸿蒙生态的发展空间非常广阔,期待你做出更多有趣的应用。
下一步可以挑战更复杂的项目——结合网络请求的天气应用、使用 Canvas 绘图的白板应用、或者接入华为帐号服务的登录应用。鸿蒙生态的开发空间非常广阔。
更多推荐



所有评论(0)