ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS ArkTS实战:从零实现一个持久化待办清单

HarmonyOS ArkTS实战:从零实现一个持久化待办清单 鸿蒙开发圈子有个不成文的传统新手入门第一个完整项目多半是待办清单。我在HarmonyOS6上用ArkTS把这个案例完整跑通了一遍从界面布局到交互逻辑再到数据持久化全部走了一遍。别看功能简单输入一条待办、勾选完成、按状态筛选、杀掉应用再打开数据还在这一套下来ArkTS声明式UI、状态管理、布局容器和本地存储这几个核心知识点全都能串起来。如果你正准备学鸿蒙开发或者已经在看ArkTS但不知道从哪个项目下手这篇文章正好适合你。我会把设计思路、关键代码和实际踩过的坑一层层展开保证不是贴一段教科书代码就完事而是能让你自己动手复现并理解背后原理的实战记录。1. 整体设计思路待办清单为什么值得认真做1.1 麻雀虽小五脏俱全的技术覆盖面很多人觉得待办清单太简单没什么技术含量但我恰恰推荐用它来入门ArkTS。原因是它的复杂度刚好卡在一个临界点上再简单一点比如只写一个静态页面你接触不到交互和状态再复杂一点比如商城或社交App你又容易被网络请求、路由、多模块这些东西淹没反而不利于聚焦核心语法。一个待办清单至少覆盖了四条主线。列表渲染对应ArkTS里的List和ForEach这是几乎所有业务页面都会用到的能力输入和点击事件对应TextInput、Button、Checkbox这些基础组件的使用帮助你理解事件绑定和状态回写数据变更后的刷新对应State状态管理机制这是ArkTS声明式UI和传统命令式编程最大的区别应用重启后数据还在对应本地持久化也就是Preferences存储。把这四条线走通你再去写其他应用套路基本都是同一个。这也解释了为什么很多培训课程和官方示例都把这个案例作为标配。它不是拿来充数的而是经过验证的、能最大程度覆盖核心知识点的最小可运行项目。1.2 从模块拆分到代码组织项目整体上我采用单页面架构入口就是Index页面但内部做了一个简单的代码分层。数据模型单独放在model目录下用来定义TodoItem这个类包含id、标题、完成状态、创建时间这些字段。这样做的原因不只是“代码整洁”更重要的是让状态有了明确的类型约束。ArkTS本身是TS的超集类型声明能帮你发现很多低级错误比如传错字段、漏掉必填参数编译阶段就会直接报错。业务组件层面我在项目里做了两个级别的拆分。页面根组件负责持有todoList数组和currentTab筛选状态统一调度数据变更具体的输入框、列表项、删除按钮这些UI细节则放在页面内部通过函数和Builder拆开避免单文件过于臃肿。这种拆分策略适合中小型项目既不会因为文件太多增加理解成本又能保证每个函数的职责单一。持久化工具我单独封装了一个StorageManager模块专门负责读写Preferences。这样做的核心原因是把数据访问和UI逻辑解耦。页面里只管调用保存和加载方法不关心数据到底存在哪里、以什么格式存储。以后如果想把本地存储换成分布式键值库或者云端接口只需要替换这一个模块页面代码一行都不用动。1.3 技术选型的几个关键判断做这个项目时我有几个明确的技术判断这里摊开说一下。第一状态管理用最基础的State不引入复杂的全局状态库。待办清单的数据都集中在页面内部父子组件之间没有共享数据的强烈需求引入AppStorage或者全局状态管理反而会增加认知负担。官方提供的State和计算属性足够支撑整个业务逻辑。第二列表筛选用Tabs组件实现。很多教程会用自定义按钮组来切换筛选状态但我选Tabs是因为它天然具备状态保持和滑动切换能力代码可读性也更好。用户左右滑动就能切换全部、进行中、已完成三个视图交互体验更接近系统级应用的感觉。第三持久化用Preferences而不是关系型数据库。这是基于数据规模的判断待办事项通常只有几十上百条属于轻量级键值数据用数据库属于杀鸡用牛刀。Preferences底层是XML文件读写速度足够快API设计也简单符合同类型App的实际需求。2. 开发环境准备与工程搭建2.1 DevEco Studio与SDK准备工欲善其事必先利其器。开发ArkTS应用官方推荐的IDE是DevEco Studio它基于IntelliJ IDEA平台Keymap和快捷键习惯都和Android Studio、WebStorm类似上手成本不高。在HarmonyOS6时代我建议直接安装最新的稳定版DevEco Studio不要用太老的版本因为API版本和SDK版本之间是有绑定关系的。创建工程时选择Empty Ability模板语言选ArkTS随后IDE会自动配置好构建链。首次打开工程时依赖下载和Gradle构建会比较慢尤其是网络状况一般的时候可能要等几分钟甚至更久这是正常现象。这里有一个新手特别容易误会的地方开发鸿蒙应用并不需要把电脑系统改成鸿蒙更不需要去刷开源鸿蒙的系统镜像。很多人被网上下载开源鸿蒙系统的话题带偏了以为必须在那样的环境里才能开发。实际上只要电脑能跑DevEco Studio配合模拟器或者手机真机就可以完成全部开发调试工作。开发环境和运行环境是两回事把这一点搞清楚能帮你少走很多弯路。2.2 创建工程与理解入口工程创建完成后建议先打开entry/src/main/ets目录把目录结构过一遍。入口模块是entry里面默认包含entryability和pages两个核心目录。EntryAbility是应用的能力入口它的onWindowStageCreate回调值得重点关注。很多初学者第一次看到这个函数会有点懵不知道loadContent是干什么的。简单理解就是应用启动后系统把窗口窗口建好了你要在这个回调里告诉窗口加载哪个页面作为首页。代码大概是下面这样import { AbilityConstant, UIAbility, Want } from kit.AbilityKit; import { window } from kit.ArkUI; export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent(pages/Index); } }这个回调机制的好处在于它把页面加载逻辑和应用生命周期解耦了。你在写复杂应用时可以在窗口创建后做很多初始化操作比如设置全局变量、加载首屏配置然后再决定加载哪个页面。理解了这个链路后续做路由跳转和页面传值会顺很多。2.3 数据模型与状态管理基础先定义TodoItem数据模型字段不需要多够用就行。我最终用的是id、title、done、createTime四个字段代码在model/TodoItem.ets文件里export class TodoItem { id: string; title: string; done: boolean; createTime: number; constructor(id: string, title: string, done: boolean, createTime: number) { this.id id; this.title title; this.done done; this.createTime createTime; } }id字段非常重要后续做删除和状态切换时它才是数据变更的唯一依据。很多人习惯用数组下标操作数据但一旦涉及筛选后的列表或者删除操作下标很容易错位用id就能彻底避开这个坑。页面里的核心状态就三个todoList数组、currentTab当前选中的Tab索引、inputValue输入框的值。分别用State修饰。State是ArkTS中最基础的状态装饰器它告诉UI框架这个变量变了依赖它的组件必须重新渲染。这是声明式UI的核心思想你不需要写类似setText、notifyDataChange这样的命令式代码框架会自动帮你完成界面更新。3. 核心界面布局与交互实现3.1 顶部输入区Row、TextInput与Button界面顶部是输入区域我的布局方案是用Column做根容器内部从上到下依次排列输入行、Tabs、底部统计栏。输入行用Row横向排列左侧是TextInput占满剩余宽度右侧是添加按钮。TextInput的配置点有两个。placeholder属性提示用户输入内容onChange事件实时同步输入内容到inputValue这样按钮点击时才能拿到最新值。这里有一个细节回车键添加事项是通过onSubmit回调实现的在手机软键盘上按回车或确定键时会触发。添加按钮的逻辑需要做一次输入校验空字符串和纯空格都不能加入列表。处理方式是先trim去掉首尾空格判断长度再构造新TodoItem加入数组。代码逻辑看着不长但边界处理很重要否则用户狂点添加按钮列表里会出现一堆空白条目体验很差。addTodo(): void { const title this.inputValue.trim(); if (!title) { return; } const item new TodoItem(Date.now().toString(), title, false, Date.now()); this.todoList [...this.todoList, item]; this.inputValue ; }注意这里我用了展开运算符创建新数组而不是直接push。这会牵扯到后面要讲的列表刷新机制是ArkTS不可变数据思维的一个直接体现。3.2 中部列表与Tabs筛选列表区域我用Tabs组件包了三个TabContent分别对应全部、进行中、已完成。Tabs的当前索引通过onChange事件同步到currentTab变量。每个TabContent内部的渲染逻辑并不写死而是根据currentTab动态计算过滤后的列表。过滤逻辑我放在计算属性里用getter实现。全部Tab返回原始todoList进行中返回done为false的项已完成返回done为true的项。这样做的好处是数据源只有一个筛选只是视图层的数据变换你修改原始数据时所有Tab的展示自动同步更新。List组件负责承载列表内部用ForEach渲染TodoItemView。ArkUI的ForEach语法和普通循环不太一样它需要三个参数数据源、item生成器、key生成器。key生成器直接返回item.id让每一项都拥有稳定的身份标识。Tabs({ barPosition: BarPosition.Start }) { TabContent() { this.buildList(this.todoList); }.tabBar(全部) TabContent() { this.buildList(this.todoList.filter(item !item.done)); }.tabBar(进行中) TabContent() { this.buildList(this.todoList.filter(item item.done)); }.tabBar(已完成) } .onChange((index: number) { this.currentTab index; })把buildList抽出来是为了避免三个TabContent内部写三遍相同代码。鸿蒙的Tabs布局很适合这种分类筛选场景底部或顶部的tabBar加上内容区的联动切换几乎是系统级的交互范式。3.3 勾选完成与滑动删除的实现勾选完成是待办清单里最核心的交互。每一行用Checkbox组件展示完成状态Checkbox的select属性绑定item.done用户点击时触发onChange事件。这里有一个容易踩坑的重点不能直接修改item.done属性然后指望界面刷新。因为TodoItem是一个普通class不是Observed装饰的观察类。如果你直接写item.done isSelect数组本身没有变化UI框架感知不到任何状态变更界面纹丝不动。正确做法是生成新对象数组让todoList的引用发生变化强制触发刷新toggleTodo(id: string): void { this.todoList this.todoList.map(item { if (item.id id) { return new TodoItem(item.id, item.title, !item.done, item.createTime); } return item; }); this.persist(); }删除操作的交互我选择了滑动删除符合移动端用户习惯。ListItem的swipeAction属性支持配置左侧和右侧的滑动操作区域我在end区域放了一个红色的删除按钮滑动后点击按钮完成删除。删除逻辑同样用id定位通过filter过滤掉目标项deleteTodo(id: string): void { this.todoList this.todoList.filter(item item.id ! id); this.persist(); }滑动删除的配置因SDK版本差异API名称可能略有调整但核心思路一致把删除按钮预置在列表项的一侧滑动后才能看到。如果你正在用的SDK版本不支持某个属性名查一下对应版本的接口文档即可。3.4 底部统计栏和页面整体适配底部统计栏我放了一个简单统计信息总条数、已完成条数、未完成条数。这一栏本身不复杂但它的布局方式值得说一说。我采用的方案是页面根容器Column列表区域用LayoutWeight权重占满中间空间底部统计栏自然沉底。这是一种非常实用的弹性布局策略不需要计算具体高度也不怕不同屏幕尺寸的适配问题。与之相对应的另一种方案是RelativeContainer相对布局适合做类似悬浮球、吸附底部的元素通过alignRules把组件锚定在容器底部。实际项目里两者各有各的适用场景。如果你的界面不复杂Column的权重分配已经足够如果在复杂页面里要处理多个元素的相对位置关系RelativeContainer会更灵活。还要注意安全区适配。在全面屏手机上底部可能存在系统导航条直接靠底的内容会被遮挡。处理方法是给底部统计栏加上安全区padding或者在页面级别通过expandSafeArea处理。这个细节很多新手不会留意但实机跑起来就会发现差距。4. 本地持久化让待办重启后还在4.1 数据存储方案的选型对比内存里的数组只能活在App运行期间一旦杀掉进程重新启动辛辛苦苦输入的待办就全没了。对一个真实的待办工具来说这是不能接受的缺陷所以持久化是本项目必须完成的一步。鸿蒙开发的数据存储方案主要有这么几个Preferences键值存储、关系型数据库、文件存储。如果只是存一个待办列表最轻量的是Preferences它本质上是一个XML文件以键值对形式保存数据。数据量不大、结构简单、读多写少的场景它都非常合适如果数据量庞大、查询条件复杂才需要上关系型数据库如果是图片、视频这类二进制大文件则是文件系统的主场。各方案对比我直接给一个选择建议待办清单这个项目毫不犹豫选Preferences。4.2 Preferences存取的核心写法Preferences的使用流程可以总结为获取存储实例、写入或读取键值、调用flush落盘。下面是我封装好的StorageManager模块核心代码import { preferences } from kit.ArkData; import { common } from kit.AbilityKit; import { TodoItem } from ../model/TodoItem; const STORE_NAME todo_store; const KEY_TODO_LIST todo_list; class StorageManager { async saveTodoList(context: common.UIAbilityContext, list: TodoItem[]): Promisevoid { const store await preferences.getPreferences(context, STORE_NAME); await store.put(KEY_TODO_LIST, JSON.stringify(list)); store.flush(); } async loadTodoList(context: common.UIAbilityContext): PromiseTodoItem[] { const store await preferences.getPreferences(context, STORE_NAME); const value await store.get(KEY_TODO_LIST, []); const parsed JSON.parse(value as string); return parsed.map((item: TodoItem) new TodoItem(item.id, item.title, item.done, item.createTime) ); } } export const storageManager new StorageManager();这里我想特别提醒两个点。第一数组不能直接塞进Preferences需要JSON.stringify序列化成字符串再保存读取时再JSON.parse解析回来。第二put之后记得调用flush。Preferences的写入分为内存写入和磁盘落盘两步flush才是真正触发持久化的操作只put不flush应用异常退出时数据可能还留在内存里没写进文件。初始化加载的时机我放在页面aboutToAppear生命周期里。这个回调在页面即将创建时触发适合做数据加载操作。加载完成后赋值给todoList页面自动渲染出上次保存的内容。4.3 状态与存储同步的工程化处理存储方案定下来之后接下来要考虑的问题是在哪些时机保存数据、怎么保存才能避免频繁写盘影响性能。我定的规则是每次数据变更后立即调用持久化。数据变更只有三个入口新增、切换完成状态、删除这三个操作完成后统调用saveTodoList。这个规则简单粗暴但它保证了任何时刻用户退出应用数据都是最新的。真正埋坑的是异步时序问题。Preferences的写入是异步的如果用户在极短时间内连续执行添加、删除等操作多个写入任务并发执行理论上可能出现覆盖问题。实际开发中我更推荐做一个简单的串行化处理用一个Promise队列串起每次保存确保上一次写入完成后再发起下一次。数据量小的时候这个问题不常见但在写消费类应用时养成这种意识能少踩很多并发相关的坑。读取时还有一个防御性设计。如果Preferences里没有存储记录get方法会返回默认值的空字符串数组JSON.parse后json数组不会有任何问题但如果存储数据损坏JSON.parse会直接抛异常。稳妥的做法是用try-catch包住解析过程解析失败时返回空数组避免应用启动直接崩溃。5. 实测踩坑记录与排查思路5.1 勾选后列表不刷新这是整个项目里我遇到最典型的问题也是很多ArkTS新手绕不过去的坎。现象是Checkbox点击之后选中状态确实变了但列表UI没有任何反应甚至Checkbox本身的勾选状态都没变。排查思路其实很清晰先确认数据有没有变再看UI有没有重新渲染。我在toggleTodo方法里加了一行日志打印todoList长度和对应项的done值确认数据变了。既然数据变了UI不刷新问题一定出在状态绑定上。最终定位到根因直接修改对象属性和通过新数组替换引用在ArkTS里是两种完全不同的状态变更方式。普通class实例的属性变化UI框架是感知不到的必须让State修饰的数组本身发生变化。改用map生成新数组后问题立刻解决。如果你用的是Observed装饰的数据类还可以配合ObjectLink在子组件里监听属性变化但对我来说新数组替换的方案最直观也不容易出幺蛾子。5.2 Tabs切换后数据错乱第二个让我头疼的问题是Tabs切换后进行中列表里出现了已完成的事项。我最初的写法是在TabContent里直接用index操作原始todoList筛选逻辑和索引判断混在一起稍微一环接错数据就串了。最后的解决方案就是回到我前面写的那种干净结构把过滤逻辑收敛成计算属性让TabContent内部只负责展示传入的列表不要在里面写复杂判断。筛选状态由currentTab统一管理数据源始终是todoList各Tab看到的只是它的不同视图。这样既不会出现数据错乱后续想增加一个延期标签只需要新增一个Tab和对应过滤条件改动成本非常低。5.3 Preferences写入后读取不到有段时间我发现程序重启后待办列表偶尔是空的但再操作一次又能正常显示。这种情况非常令人抓狂因为问题不是必现的很看运气。排查下来有两个原因。第一个是没有及时调用flush导致数据还留在内存缓存中就退出了下次启动读到的是旧文件。第二个是因为我在保存时把一个Pending任务挂在队列里就立刻启动了读取流程读操作跑在了写操作的前面。这两个问题合在一起的教训是写和读都要遵循同一个异步时序写操作确保落盘完成读操作再读取数据。我的最终改法是保存时await整个保存流程完成加载时明确依赖上一次保存已经结束彻底避免了竞态条件。5.4 模拟器、预览器和真机的配合使用开发调试过程中我还对鸿蒙提供的三种调试环境做了个对比。预览器Previewer启动最快改动代码几乎秒级刷新适合布局调试和组件样式微调但它不是完整的运行时环境很多系统能力是模拟不出来的。模拟器完整模拟了HarmonyOS运行环境适合验证交互逻辑和数据持久化启动速度和真机相比快很多是我日常开发的主力环境。真机调试最接近最终用户体验但每次构建需要签名和连接相对繁琐我一般是在上架前做最终验证才切到真机。建议你也按照这个节奏来布局用预览器逻辑用模拟器最终验证用真机。千万别一直用真机跑否则每次改一行UI都要等构建部署非常浪费时间。5.5 下一步扩展的几个方向待办清单完成之后如果你的学习节奏是持续推进我建议在现有代码上做这样几个方向的扩展每一个都对应真实项目中的常用能力。第一个方向是分类和标签。给TodoItem加category字段再用二级Tabs或者弹窗筛选这能帮你把数据结构设计和复杂筛选能力练熟。第二个方向是设置提醒。可以结合通知和定时任务能力在待办时间到达时发送本地通知这一块能让你接触到系统服务集成。第三个方向是云同步。把本地列表同步到网络端这需要引入网络请求和用户身份模块是一个从单机应用走向联机应用的很好练习。这些扩展的共同特点是不推翻现有架构只是在数据模型和业务逻辑上叠加新能力。这意味着你现在的分层设计是经得起推演的后续学习新知识点时可以把老项目当成试验田而不是每次从零开始。这个案例做下来我最大的体会是ArkTS和声明式UI的思维方式和过去写传统页面时完全不同你必须习惯从数据角度而不是控件角度思考问题。先设计数据模型再考虑UI怎么展示、事件怎么改变数据最后才是布局和样式。只要这个思维转过来再看别的鸿蒙项目源码会通畅很多。希望这篇实战记录能帮你把自己的第一个鸿蒙待办清单顺利跑起来。
返回列表