在 HarmonyOS 5 的应用开发里,ArkUI 负责界面构建,状态管理负责驱动 UI 刷新,而本地数据持久化则决定了应用是否真正“可用”。官方文档明确将 ArkUI 作为 HarmonyOS 跨设备应用的 UI 框架,同时将用户首选项 Preferences 定位为适合本地轻量级 Key-Value 数据存储的方案。基于这些能力,本文实现一个“专注清单”小应用:支持新增任务、完成任务、删除任务和本地持久化,重启应用后数据仍然保留。
关键词
HarmonyOS 5、ArkTS、ArkUI、状态管理、Preferences、APP 开发
一、为什么选这个实战题目
刚开始学习 HarmonyOS 开发时,很多人一上来就写复杂业务,结果把页面、状态、存储、交互全堆在一起,最后不仅难调试,文章也容易写得很散。相比之下,一个“小而完整”的清单类 APP 更适合做实战案例:它既能覆盖 ArkUI 的声明式界面开发,也能覆盖状态管理和数据持久化,是一个很适合入门到进阶过渡的题目。ArkUI 的核心思路就是“数据驱动 UI”,官方状态管理文档也强调了状态变化与界面更新之间的联动关系。
我这次实现的不是一个“只能演示”的 Demo,而是一个真正能落地的小工具。用户打开应用后,可以输入当天要完成的任务;点击“完成”后任务状态会立即变化;点击“删除”后任务会从列表中移除;再次启动应用时,之前的数据还能自动恢复。这样的例子虽然小,但已经具备一个真实 APP 的基本骨架。
二、技术选型与版本说明
本文项目基于 HarmonyOS 5 系列 的开发思路,使用 ArkTS + Stage Model + ArkUI 来完成页面开发。官方的 HarmonyOS 5.0.0 Release 文档说明,这一版本以 API 12 Release 为核心;后续 5.0.1 Release 则继续增强了 API 13 的能力与稳定性,因此本文的代码思路适用于 HarmonyOS 5.0.0 及以上版本的常规学习与实践。
在本地存储方案上,我没有直接上数据库,而是选用了 Preferences。原因很简单:Preferences 本身就是为“少量、本地、轻量级”的 Key-Value 数据而设计的,访问速度快,使用成本低,非常适合存储任务列表、主题配置、登录状态、开关项这类数据。官方文档也明确指出,如果是大量数据场景,则应考虑键值型数据库或关系型数据库。
三、先看最终功能
这个专注清单 APP 包含 4 个基础功能:
- 新增任务
- 标记任务完成 / 未完成
- 删除任务
- 使用 Preferences 持久化保存任务列表
从教学角度看,这 4 个功能已经足够覆盖 HarmonyOS APP 开发中最核心的三件事:页面布局、状态变化、数据落盘。
四、项目结构设计
为了让代码更清晰,我把项目拆成 3 个部分:
entry/src/main/ets/
├── model/
│ └── TodoItem.ets
├── common/
│ └── TodoStore.ets
└── pages/
└── Index.ets
其中:
TodoItem.ets:定义任务数据结构TodoStore.ets:封装 Preferences 的读写逻辑Index.ets:主页面,负责 UI 展示和交互
这样的分层虽然简单,但很适合后期扩展。后面如果你要加统计页、详情页、提醒页,基本不用推翻重写。
五、定义任务模型
先定义一个任务对象,字段尽量保持简洁:
// model/TodoItem.ets
export interface TodoItem {
id: number
title: string
done: boolean
createTime: string
}
这里的设计很直接:
id:任务唯一标识,便于更新和删除title:任务内容done:是否完成createTime:创建时间,方便后续展示和排序
六、封装本地持久化:Preferences
Preferences 非常适合这类小体量数据。官方文档将它定义为轻量级 Key-Value 数据处理能力,用于本地少量数据的持久化;相关接口需要先通过 preferences.getPreferences 获取实例后再调用。
下面是一个简单可复用的存储封装:
// common/TodoStore.ets
import type common from '@ohos.app.ability.common'
import {
preferences } from '@kit.ArkData'
import {
TodoItem } from '../model/TodoItem'
const STORE_NAME: string = 'focus_todo_store'
const KEY_TODO_LIST: string = 'todo_list'
export class TodoStore {
private static async getStore(
context: common.UIAbilityContext
): Promise<preferences.Preferences> {
return await preferences.getPreferences(context, STORE_NAME)
}
static async save(
context: common.UIAbilityContext,
list: TodoItem[]
): Promise<void> {
const store = await TodoStore.getStore(context)
await store.put(KEY_TODO_LIST, JSON.stringify(list))
await store.flush()
}
static async load(
context: common.UIAbilityContext
): Promise<TodoItem[]> {
const store = await TodoStore.getStore(context)
const rawValue = store.getSync(KEY_TODO_LIST, '[]') as string
try {
return JSON.parse(rawValue) as TodoItem[]
} catch (error) {
return []
}
}
}
这里有两个关键点
第一,任务列表是数组,而 Preferences 更擅长存储简单值,所以我这里采用 JSON 字符串化 的方式保存。对于轻量场景,这是非常常见也非常实用的处理方式。
第二,写入后我调用了 flush()。很多初学者只 put() 不 flush(),结果以为自己保存成功了,重启应用才发现数据没有稳定落盘。这个细节很小,但实际开发里很关键。
七、主页面开发:让界面真正“活起来”
ArkUI 的声明式开发方式,本质上是“状态决定界面”。只要状态变了,界面就会按声明重新渲染,这也是 HarmonyOS UI 开发的核心体验之一。
下面开始写主页面:
// pages/Index.ets
import type common from '@ohos.app.ability.common'
import {
TodoItem } from '../model/TodoItem'
import {
TodoStore } from '../common/TodoStore'
@Entry
@Component
struct Index {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext
@State inputText: string = ''
@State todoList: TodoItem[] = []
aboutToAppear(): void {
this.loadTodoList()
}
private async loadTodoList(): Promise<void> {
this.todoList = await TodoStore.load(this.context)
}
private async persist(): Promise<void> {
await TodoStore.save(this.context, this.todoList)
}
private get totalCount(): number {
return this.todoList.length
}
private get finishedCount(): number {
let count = 0
for (let i = 0; i < this.todoList.length; i++) {
if (this.todoList[i].done) {
count++
}
}
return count
}
private formatNow(): string {
const date = new Date()
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
const h = date.getHours().toString().padStart(2, '0')
const min = date.getMinutes().toString().padStart(2, '0')
return `${
y}-${
m}-${
d} ${
h}:${
min}`
}
private async addTodo(): Promise<void> {
const title = this.inputText.trim()
if (!title) {
return
}
const newItem: TodoItem = {
id: Date.now(),
title: title,
done: false,
createTime: this.formatNow()
}
this.todoList = [newItem, ...this.todoList]
this.inputText = ''
await this.persist()
}
private async toggleTodo(id: number): Promise<void> {
const newList: TodoItem[] = []
for (let i = 0; i < this.todoList.length; i++) {
const item = this.todoList[i]
if (item.id === id) {
newList.push({
id: item.id,
title: item.title,
done: !item.done,
createTime: item.createTime
})
} else {
newList.push(item)
}
}
this.todoList = newList
await this.persist()
}
private async deleteTodo(id: number): Promise<void> {
const newList: TodoItem[] = []
for (let i = 0; i < this.todoList.length; i++) {
if (this.todoList[i].id !== id) {
newList.push(this.todoList[i])
}
}
this.todoList = newList
await this.persist()
}
@Builder
TodoCard(item: TodoItem) {
Row() {
Column({
space: 6 }) {
Text(item.title)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.decoration({
type: item.done ? TextDecorationType.LineThrough : TextDecorationType.None
})
.opacity(item.done ? 0.55 : 1)
Text(`创建时间:${
item.createTime}`)
.fontSize(12)
.fontColor('#666666')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Button(item.done ? '取消完成' : '完成')
.height(34)
.fontSize(13)
.margin({
right: 8 })
.onClick(() => {
this.toggleTodo(item.id)
})
Button('删除')
.height(34)
.fontSize(13)
.onClick(() => {
this.deleteTodo(item.id)
})
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor(item.done ? '#F3F5F7' : '#FFFFFF')
}
build() {
Column({
space: 16 }) {
Column({
space: 8 }) {
Text('专注清单')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
Text(`共 ${
this.totalCount} 项,已完成 ${
this.finishedCount} 项`)
.fontSize(14)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Row() {
TextInput({
placeholder: '输入今天最重要的一件事',
text: this.inputText
})
.layoutWeight(1)
.height(48)
.onChange((value: string) => {
this.inputText = value
})
Button('添加')
.height(48)
.margin({
left: 10 })
.onClick(() => {
this.addTodo()
})
}
.width('100%')
Scroll() {
Column({
space: 12 }) {
if (this.todoList.length === 0) {
Column() {
Text('当前还没有任务')
.fontSize(18)
.fontColor('#666666')
Text('先添加一条任务,开始今天的专注吧')
.fontSize(13)
.fontColor('#999999')
.margin({
top: 8 })
}
.width('100%')
.padding({
top: 60, bottom: 60 })
} else {
ForEach(this.todoList, (item: TodoItem) => {
this.TodoCard(item)
}, (item: TodoItem) => item.id.toString())
}
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#F7F8FA')
}
}
八、这段代码到底做了什么
1. @State 负责驱动页面刷新
@State inputText: string = ''
@State todoList: TodoItem[] = []
这两个变量是页面最核心的状态。输入框内容变化时,inputText 会变;新增、完成、删除任务时,todoList 会变。一旦状态变化,界面会自动跟着更新,这就是声明式 UI 最省心的地方。
2. aboutToAppear() 负责首次加载数据
aboutToAppear(): void {
this.loadTodoList()
}
页面创建完成后,我立刻从 Preferences 里读取本地数据,这样用户每次进入应用都能看到上次保存的任务列表。
3. 对列表更新时,我采用“新数组替换旧数组”的写法
比如切换完成状态时,我没有直接在原数组里做复杂原地修改,而是构造了一个新的数组再整体赋值给 todoList。这么写的好处是逻辑更清晰,也更符合状态驱动思维,后期扩展排序、筛选时也更稳。
4. 每次用户操作后都立刻持久化
await this.persist()
这一步很重要。很多 Demo 只做界面效果,不做真正落盘,一旦退出应用就全部丢失。这里每次新增、切换、删除后都立即写入 Preferences,应用的“完成度”就上来了。
九、为什么这里不用数据库
这个问题很常见:既然是数据,为什么不用关系型数据库?
答案很简单:没必要。
官方对 Preferences 的定位已经很明确,它适合本地少量、轻量级 Key-Value 数据;而大量数据或复杂关系场景,则更适合键值型数据库或关系型数据库。我的这个清单 APP,本质上只是保存一个小列表,没有复杂查询、联表、分页、事务这类需求,所以 Preferences 是更轻、更快、更合理的选择。
技术选型并不是“谁高级就用谁”,而是“谁更适合当前场景就用谁”。这也是我认为写技术文章时特别值得强调的一点。
十、可以继续扩展的方向
到这里,这个 APP 已经能正常跑起来了。但如果你想把文章写得更有深度,还可以继续扩展:
1. 增加任务分类
例如把任务分成“学习”“工作”“生活”三类,这样列表会更接近真实产品。
2. 增加筛选能力
比如“全部 / 未完成 / 已完成”三种视图,能进一步体现状态管理的价值。
3. 增加页面跳转
后续你完全可以把本文的单页应用扩展成“首页 + 统计页 + 设置页”的结构。
4. 增加主题切换
深色模式、卡片风格、专注颜色切换,都很适合继续往 ArkUI 的视觉表达上延展。
十一、这次实战最大的收获
做完这个小项目后,我对 HarmonyOS APP 开发有三个更清晰的认识。
第一,ArkUI 的核心不是“写组件”,而是“写状态和界面的关系”。
第二,本地持久化一定要尽早纳入设计。很多初学项目只在内存里跑,表面上功能都有,实际上应用一重启就归零,这样的 Demo 很难称得上完整。
第三,HarmonyOS 的很多能力并不难,难的是把它们用在合适的地方。像 Preferences 这种能力,看起来不“炫”,但恰恰最能体现真实开发思路。
十二、总结
本文基于 HarmonyOS 5 的开发思路,完成了一个极简但完整的专注清单 APP。它虽然不大,却串起了 HarmonyOS APP 开发里非常关键的三部分:ArkUI 页面搭建、状态管理驱动刷新、Preferences 本地持久化存储。
对我来说,这类项目最大的价值并不在于“做了一个待办清单”,而在于它让我真正把 HarmonyOS 的开发链路走通了一遍。一个能保存数据、能响应交互、能稳定运行的 APP,哪怕功能简单,也比空有界面的展示页更有技术含量。后续如果继续往多页面、分类管理、跨设备协同等方向扩展,这个项目还可以自然成长为一个更完整的 HarmonyOS 应用。
参考资料
- HarmonyOS 5.0.0 Release 版本说明
- HarmonyOS 5.0.1 Release 版本说明
- ArkTS / Stage Model 应用开发文档
- ArkUI 状态管理文档
- Preferences 用户首选项文档
- 页面路由文档