HarmonyOS 5 实战:用 ArkUI + Preferences 做一个极简专注清单 APP

简介: 本文基于HarmonyOS 5,使用ArkTS+ArkUI实现轻量级“专注清单”APP:支持任务增删改、状态切换及Preferences本地持久化,重启不丢数据。覆盖UI构建、状态驱动与数据落盘三大核心开发能力,是入门到进阶的理想实战案例。(239字)

在 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 个基础功能:

  1. 新增任务
  2. 标记任务完成 / 未完成
  3. 删除任务
  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 应用。


参考资料

  1. HarmonyOS 5.0.0 Release 版本说明
  2. HarmonyOS 5.0.1 Release 版本说明
  3. ArkTS / Stage Model 应用开发文档
  4. ArkUI 状态管理文档
  5. Preferences 用户首选项文档
  6. 页面路由文档
相关文章
|
5天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
10731 63
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
5天前
|
人工智能 IDE API
2026年国内 Codex 安装教程和使用教程:GPT-5.4 完整指南
Codex已进化为AI编程智能体,不仅能补全代码,更能理解项目、自动重构、执行任务。本文详解国内安装、GPT-5.4接入、cc-switch中转配置及实战开发流程,助你从零掌握“描述需求→AI实现”的新一代工程范式。(239字)
3111 126
|
1天前
|
人工智能 自然语言处理 供应链
【最新】阿里云ClawHub Skill扫描:3万个AI Agent技能中的安全度量
阿里云扫描3万+AI Skill,发现AI检测引擎可识别80%+威胁,远高于传统引擎。
1199 1
|
11天前
|
人工智能 JavaScript API
解放双手!OpenClaw Agent Browser全攻略(阿里云+本地部署+免费API+网页自动化场景落地)
“让AI聊聊天、写代码不难,难的是让它自己打开网页、填表单、查数据”——2026年,无数OpenClaw用户被这个痛点困扰。参考文章直击核心:当AI只能“纸上谈兵”,无法实际操控浏览器,就永远成不了真正的“数字员工”。而Agent Browser技能的出现,彻底打破了这一壁垒——它给OpenClaw装上“上网的手和眼睛”,让AI能像真人一样打开网页、点击按钮、填写表单、提取数据,24小时不间断完成网页自动化任务。
2563 6
|
25天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
24388 122