尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

鸿蒙ArkTS实战:从零构建待办清单应用全流程解析

鸿蒙ArkTS实战:从零构建待办清单应用全流程解析 待办清单这个题材在鸿蒙ArkTS开发里几乎是绕不开的第一个实战项目。它功能边界清晰、交互路径完整又刚好覆盖了状态管理、列表渲染、数据持久化这几个核心知识点拿来练手再合适不过。这篇文章我会按照自己实际开发HarmonyOS应用的流程把从环境准备、页面搭建到数据落地的完整链路拆开讲每一步都给出可直接复用的代码和设计思路。1. 为什么拿待办清单做鸿蒙ArkTS的第一个实战案例1.1 这个项目到底解决了什么问题很多人在接触ArkTS时第一反应是去翻官方文档把声明式UI语法、状态管理V1/V2、组件生命周期这些概念过一遍然后发现——看懂了但写不出来。这正是待办清单案例的价值所在它用一条完整的产品主线把零散的知识点串联起来。待办清单虽然功能简单但麻雀虽小五脏俱全。它至少包含这些关键交互列表展示、新增输入、状态切换、删除操作、数据保存。对应到ArkTS开发中就是List组件渲染、State状态管理、TextInput输入处理、条件判断、本地持久化。一个案例覆盖了日常开发80%的高频技术点做完这个项目你再看底部导航栏、详情页跳转、表单提交这些需求思路会清晰很多。从能力模型的角度讲这个项目同时也把鸿蒙开发的调试链路完整走了一遍。从DevEco Studio的Previewer预览、模拟器运行到真机调试每个环节都会遇到不同的问题提前把这些坑踩一遍后面做正式项目会顺畅得多。1.2 项目方案选型背后的考量在动手写代码之前我习惯先做方案选型而不是打开IDE就开始敲。这里面有几个关键的取舍点值得说。存储方案选型。待办清单数据量小、结构简单我一开始在首选项Preferences和关系型数据库RelationalStore之间犹豫。前者是键值对存储适合轻量级数据后者是SQLite封装适合结构化查询。最终我选了首选项原因是待办条目数据量通常百条以内只需要按ID读取、整体写入不需要复杂查询和关联表。如果做多用户、多分类的复杂场景才需要考虑数据库。这个决定简化了代码量也让初学者可以更聚焦在UI和状态管理上。状态管理方案选型。HarmonyOS ArkTS提供V1State/Prop/Link和V2ObservedV2/Trace等两套装饰器体系。考虑到当前生态稳定性和资料齐全程度我用了V1体系。虽然V2在性能优化和嵌套观察上有优势但V1的学习曲线更平缓社区资料也更多。等你把V1体系跑通理解了状态驱动UI更新这套心智模型再切V2几乎没有成本。布局方案选型。页面布局我混合使用了Column、Row和List。虽然热词里有人提到RelativeContainer和Flex但我个人认为待办清单这种场景Column Row的线性布局足够清晰Flex适合更复杂的流式布局RelativeContainer适合绝对定位场景。新手阶段不要把布局玩出花把线性布局吃透能解决90%的页面难题。2. 环境准备与工程初始化2.1 开发环境搭建的注意事项鸿蒙开发要从官方渠道下载DevEco Studio版本选择上我建议直接用最新的稳定版因为HarmonyOS SDK版本迭代很快老版本IDE打开新工程时会出现SDK不兼容的提示。工程创建时选择Empty Ability模板即可模板会在entry/src/main/ets下生成entryability和pages两个目录这是我们所有代码的主战场。有两点值得注意语言选择ArkTS不要选JS。虽然IDE支持两种语言但ArkTS在API调用和类型约束上更严谨也是鸿蒙开发当前的主流方向。设备类型选择Phone即可Tablet和2in1的适配逻辑后续可以再扩展初次开发不要给自己增加负担。2.2 数据模型设计数据模型是整个项目的基石我建议在正式写UI之前先把数据类型定义好。在src/main/ets/models/目录下新建TodoModel.ets文件// TodoModel.ets export class TodoItem { id: number 0; // 唯一ID content: string ; // 待办内容 isDone: boolean false; // 是否完成 createTime: number 0; // 创建时间戳 }这里有几个设计细节值得强调。id字段我在循环渲染时用id作为key。ArkTS的ForEach要求key必须唯一且稳定直接使用content做key会遇到两个问题——待办内容可能重复导致渲染异常内容修改后key变化会导致组件重建而不是更新性能变差。所以id是必须的。createTime字段这个字段用于排序和后续可能的“按时间筛选”功能。虽然当前版本用不到但数据结构先行可以避免后期为加字段而重构整个存储逻辑。isDone布尔标志状态切换直接用true/false即可不需要用数字或字符串。布尔语义清晰也便于直接绑定到UI的条件渲染。3. 核心页面开发实战3.1 待办首页的页面骨架首页的主要结构顶部标题栏、中间待办列表区、底部新增输入区。对应到ArkTS我用一个Column容器做根节点内部通过justifyContent设置SpaceBetween把三块区域撑开。// pages/Index.ets Entry Component struct Index { State todoList: TodoItem[] []; build() { Column() { // 标题区 Text(我的待办) .fontSize(24) .fontWeight(FontWeight.Bold) .width(100%) .padding(16) // 列表区 List() { ForEach(this.todoList, (item: TodoItem) { ListItem() { TodoListItem({ item: item }) } }, (item: TodoItem) item.id.toString()) } .layoutWeight(1) .width(100%) // 输入区 Row() { TextInput({ placeholder: 输入新的待办事项 }) .layoutWeight(1) .height(44) Button(添加) .height(44) .margin({ left: 12 }) } .width(100%) .padding(16) } .width(100%) .height(100%) } }这里的核心逻辑是State todoList。当数组发生变化时ArkUI会重新执行build方法自动更新UI。你不需要手动操作DOM这是声明式UI和传统命令式UI最大的区别。layoutWeight(1)是个容易忽略但很实用的属性它让列表区自动填充输入区和标题区之外的所有空间。3.2 自定义待办组件的拆分把ListItem拆成独立组件是良好工程习惯。父组件通过Prop或普通参数向子组件传值子组件负责渲染单条数据。// components/TodoListItem.ets Component struct TodoListItem { Prop item: TodoItem; build() { Row() { // 完成状态图标 Text(this.item.isDone ? ✓ : ○) .fontSize(24) .fontColor(this.item.isDone ? #1296DB : #999999) .width(32) // 待办内容 Text(this.item.content) .fontSize(16) .decoration({ type: this.item.isDone ? TextDecorationType.LineThrough : TextDecorationType.None, color: #999999 }) .fontColor(this.item.isDone ? #999999 : #333333) .margin({ left: 8 }) Blank() // 删除按钮 Text(删除) .fontSize(14) .fontColor(#E84026) .onClick(() { // 删除逻辑通过回调传给父组件 }) } .width(100%) .height(56) .padding({ left: 16, right: 16 }) } }关于状态切换的交互设计这里解释一下。很多人初次实现时会用Checkbox组件但Checkbox的默认样式和你自定义的需求往往有出入而且点击区域偏小。我个人更推荐用文本符号实现未完成显示空心圆“○”点击后变成实心对勾“✓”同时内容加删除线。这种方案简洁高效也没有组件默认样式的约束问题。注意Prop的语义是单向数据流。子组件不能直接修改item的isDone属性因为Prop是拷贝传递而非引用传递。所以正确的做法是把状态切换和删除操作封装成回调函数传给子组件调用。3.3 操作逻辑与数据变更父组件里定义操作方法通过函数参数传给子组件// 添加待办 addTodo(content: string) { if (content.trim() ) return; let newItem new TodoItem(); newItem.id Date.now(); newItem.content content.trim(); newItem.createTime Date.now(); newItem.isDone false; this.todoList.push(newItem); this.saveData(); } // 切换完成状态 toggleTodo(id: number) { let item this.todoList.find(it it.id id); if (item) { item.isDone !item.isDone; this.saveData(); } } // 删除待办 removeTodo(id: number) { this.todoList this.todoList.filter(it it.id ! id); this.saveData(); }这段逻辑里有几个容易踩坑的地方值得单独提一下。数组操作必须触发状态更新。ArkTS的State对数组的深浅监听有差异。push方法是“变异方法”会触发UI更新但如果你直接给数组某个索引赋值比如this.todoList[0] newItemUI不会刷新。原因在于State的数组监听依赖Proxy拦截索引赋值在部分场景下不会被检测到。所以删除时我选择了filter生成新数组并整体赋值这是更稳妥的做法。id生成的唯一性。使用Date.now()在这个场景够用因为用户不可能在同一个毫秒内添加两条待办。但如果要更严谨可以用Math.random()拼接时间戳或者引入自增计数器。生产项目中可以考虑用ohos.util.UUID生成UUID。4. 数据持久化与状态恢复4.1 首选项存储的核心用法不持久化的待办清单是耍流氓应用一关数据全丢。鸿蒙的ohos.data.preferences首选项接口设计得很直接// utils/StorageUtil.ets import preferences from ohos.data.preferences; import common from ohos.app.ability.common; const STORE_NAME todo_store; const KEY_TODO_LIST todo_list; async function getPref(context: common.Context) { return await preferences.getPreferences(context, STORE_NAME); } // 保存待办列表 export async function saveTodoList(context: common.Context, list: TodoItem[]) { let pref await getPref(context); let jsonStr JSON.stringify(list); await pref.put(KEY_TODO_LIST, jsonStr); await pref.flush(); } // 读取待办列表 export async function loadTodoList(context: common.Context): PromiseTodoItem[] { let pref await getPref(context); let jsonStr await pref.get(KEY_TODO_LIST, []); return JSON.parse(jsonStr) as TodoItem[]; }这里有几个细节原因需要说明。Async/await链路的必要性。getPreferences返回的是Promise所以所有依赖它的操作都要在异步上下文中执行。我在页面aboutToAppear生命周期里调用loadTodoList然后赋值给todoList这样应用冷启动时数据能自动恢复。flush的调用。put操作只是写入内存缓存必须调用flush才能落盘。我见过不少新手写了put没写flush测试时一切正常重启应用数据全丢排查半天找不到原因。JSON序列化的坑。JSON.stringify会把TodoItem实例转成纯JSON对象读取回来parse出来的就不是TodoItem实例了。不过因为我们不依赖类方法只访问属性字段所以类型断言as TodoItem[]足够用。如果业务复杂到需要类方法就要写自定义反序列化逻辑。4.2 页面生命周期中的数据加载在Index.ets的入口组件里需要获取UIAbilityContext。做法是在aboutToAppear中调用aboutToAppear() { let context getContext(this) as common.UIAbilityContext; loadTodoList(context).then((list: TodoItem[]) { this.todoList list; }); }关于上下文获取稍微补充一下。在页面组件中getContext(this)可以直接拿到当前组件的上下文对象。但在非组件文件中比如工具类里你无法调用getContext必须由调用方传入context。这就是StorageUtil中所有方法都接收context参数的原因。养成显式传入context的习惯会让代码的可测试性和复用性更好。4.3 待办数量的统计展示加一个统计展示会让项目看起来完整很多。在标题区域右侧或者底部区域放一个文本实时展示总计/已完成。这个UI的更新不需要额外逻辑因为它是响应式的Text(${this.todoList.length} 项 · ${this.todoList.filter(it it.isDone).length} 项已完成) .fontSize(14) .fontColor(#999999)每当todoList变化这个文本会自动重新计算。注意这里每帧都会执行filter数据量小的时候没有问题。如果待办达到千条级别可以单独维护一个State doneCount在增删和状态切换的地方手动更新。5. 常见问题与调试技巧实录5.1 列表不更新这是我在社区里看到最多的问题现象是数据变了UI没变。除了前面说的索引赋值问题还有一个高频原因是在子组件里直接修改了Prop拷贝。Prop设计为单向同步你在子组件里写this.item.isDone trueUI可能局部刷新但父组件的todoList没有变下次任何刷新都会把UI“打回原形”。排查思路先在操作函数最前面加一行console.log确认数据确实变更了再确认是否用的是push/splice/filter这类能触发监听的数组方法最后检查父子组件通信链路确保操作走到了父组件函数。5.2 Previewer预览与实际运行效果不一致这个我踩过很多次。DevEco Studio的Previewer基于本地模拟渲染和真机在某些构建上存在差异。典型场景字体渲染宽高、安全区高度、键盘弹起对布局的压缩效果。最稳妥的调试路径是Previewer快速看布局模拟器跑交互真机做最后验收。5.3 真机调试如何连接很多新人卡在“写程序容易上真机难”。在DevEco Studio中连接鸿蒙手机调试需要先在手机的设置-系统-开发者选项中打开USB调试然后用USB连接电脑手机弹窗选择“允许USB调试”。IDE会自动识别设备点击运行按钮旁的设备下拉框选择你的手机即可。如果设备不显示依次排查数据线是否支持数据传输充电线不行、是否安装了对应驱动Windows上还需要HDC工具链、手机是否解锁状态下连接。5.4 首选项读写频繁导致卡顿待办清单每操作一次就saveTodoList一次数据量小还好如果未来数据膨胀连续快速操作会感觉明显卡顿。优化方向有两个防抖合并写入用户停止操作500毫秒后再写批量操作时暂存内存在onPageHide或应用进入后台时统一落盘。5.5 关于页面栈和窗口配置的补充体会热词里有人提到windowStage.loadContent这涉及Ability加载页面时的窗口设置。HarmonyOS应用中入口Ability通过loadContent加载首页如果你想在应用启动时设置全屏、横竖屏锁定或沉浸式状态栏都是在这个阶段处理。待办清单用不到这些能力但你知道有这层控制入口后以后做视频播放、游戏这类需要屏幕控制的项目就知道从哪里入手了。6. 界面细节打磨与体验提升6.1 空列表的占位引导待办列表为空时页面下半部分显示一大片空白体验不好。加一个空态占位很简单if (this.todoList.length 0) { Column() { Text(暂无待办事项) .fontSize(16) .fontColor(#999999) Text(点击下方输入框添加第一条待办吧) .fontSize(14) .fontColor(#BBBBBB) .margin({ top: 8 }) } .layoutWeight(1) .justifyContent(FlexAlign.Center) } else { List() { // 列表渲染 } .layoutWeight(1) }这个做法的原理是条件渲染。空态和列表互斥通过length判断决定渲染哪一块。注意子组件中的写法如果用了if/else两条分支的布局属性要靠各分支自己去保证不会有“继承”的宽高设置。6.2 输入框的交互细节输入框的两个细节直接影响日常使用体验。第一个是提交后清空输入框。TextInput的文本控制器TextInputController可以拿到输入内容提交后调用controller.clear()。如果不用控制器而是通过TextInput({ text: this.inputValue })绑定状态变量则赋值this.inputValue 即可。但要注意此时输入框的每一次文本变更都要同步到状态变量用onChange回调更新。第二个是键盘右下角按钮。TextInput组件的enterKeyType属性可以设置为EnterKeyType.Done并监听onSubmit事件。这样用户在键盘上点“完成”或“对勾”就能直接提交不用再抬手去点屏幕上的“添加”按钮。这个体验优化成本极低但感知极强。6.3 排序与分组策略待办清单做到最后很多人会想加分组比如按“今天/明天/以后”或者按“未完成/已完成”分区。在ArkTS里实现分组列表最常见的做法是使用List的ListItemGroup组件每组给一个标题组内渲染各自数据。这个组件对分组展示的支撑做得不错头尾间距、吸顶效果都有参数控制。但如果需求只是“未完成排前面已完成排后面”不需要复杂分组写一个排序函数就好function sortTodoList(list: TodoItem[]): TodoItem[] { return [...list].sort((a, b) { if (a.isDone ! b.isDone) return a.isDone ? 1 : -1; return b.createTime - a.createTime; }); }这里用[...list]先做浅拷贝再排序避免sort原地修改原数组导致状态追踪出问题。排序完成后整体赋值给todoList自然触发UI刷新。7. 从待办清单到完整应用的扩展思考7.1 底部导航栏的接入思路做完待办清单很多人下一步会做带底部导航栏的多页面应用因为这在鸿蒙应用里太常见了。底部导航栏可以在首页用Tabs组件实现也可以自绘Row加图标加文本加点击事件配合State currentIndex用if切换页面。如果你把待办清单做成“首页”再做一个“统计页”展示完成率曲线一个完整的双Tab应用就出来了。7.2 数据层升级从首选项到关系型数据库待办条目量大了之后首选项整存整取的方式会显得力不从心。关系型数据库ohos.data.relationalStore支持SQL语句、分页查询、条件过滤是更工程化的方案。对于从待办清单起步的开发者当你发现你的数据查询逻辑越来越复杂或者多设备同步需求出现时就是升级数据库的时候了。这里不多展开但记住一个判断标准查询复杂度超过“按ID找一条”就应该考虑数据库了。对于刚上手ArkTS的开发者我最后的建议是不要急着去看一遍所有API再动手直接拿这个待办清单项目练遇到问题再查文档。等你把这个项目完整做下来声明式UI心智模型、状态管理、组件通信、本地存储这四个核心能力都有了再去看官方文档里那些高级话题比如V2状态管理、动效系统、分布式迁移会轻松很多。而且项目先做出来你和朋友家人聊起来也有实物可以演示——这种正反馈比看十遍文档都管用。另外建议把这个项目格式化一下把代码整理得整齐一些上传到代码托管平台。后面你每学一个新知识点比如侧滑删除、多选批量操作、日历联动都可以在这个工程上加分支实践。鸿蒙生态还在快速迭代与其追着新特性跑不如把一个项目打磨深你会发现很多API之间的联动规律是看分散的Demo看不出来的。
返回列表