HarmonyOS ArkTS待办列表开发实战:从声明式UI到数据持久化 1. 项目概述一个待办列表能有多复杂看到“待办列表”这个标题很多开发者可能会觉得这太基础了不就是增删改查吗但如果你正在接触HarmonyOS或者想从一个具体的、完整的应用来切入这个全新的分布式操作系统那么这个“待办列表”案例的价值就远超你的想象。它绝不是一个简单的Hello World而是一个绝佳的、麻雀虽小五脏俱全的HarmonyOS应用开发实战沙盘。我之所以花时间把这个案例掰开揉碎了讲是因为在HarmonyOS的语境下开发一个待办列表你需要面对的是与传统移动开发截然不同的设计理念和实现路径。它不仅仅是在屏幕上显示几条任务更涉及到ArkTS声明式UI的构建思想、应用状态的高效管理、本地数据的持久化存储以及为未来可能的跨设备流转打下基础。对于刚接触HarmonyOS的开发者来说通过这个案例你能系统性地摸清从页面布局、数据绑定、用户交互到数据存储的完整闭环。而对于有经验的开发者这个案例能帮你快速理解HarmonyOS应用模型Stage模型下的UIAbility、页面路由、以及ArkUI框架的核心特性。简单说这个项目适合所有希望从零到一构建一个可运行、结构清晰、符合HarmonyOS最佳实践的ArkTS应用的开发者。我们将一起完成一个具备添加、完成、删除、筛选待办事项并能将数据保存到本地的完整应用。你会发现HarmonyOS开发中的许多“为什么”和“怎么做”都会在这个看似简单的列表里找到答案。2. 项目整体设计与架构思路拆解在动手写代码之前我们先来聊聊设计思路。一个健壮的待办列表应用其核心在于清晰的数据流和UI响应逻辑。在HarmonyOS的ArkUI框架下我们尤其要拥抱其“声明式UI”和“状态驱动”的哲学。2.1 技术栈与框架选型为什么是ArkTS和Stage模型首先明确我们的技术栈ArkTS作为主开发语言Stage模型作为应用模型。这是HarmonyOS当前及未来的主流方向。ArkTS的选择理由ArkTS是TypeScript的超集为HarmonyOS做了深度优化。它继承了TS的静态类型检查和现代语法特性能极大提升开发效率和代码可维护性。对于待办列表这类涉及状态频繁变更的应用ArkTS的强类型能帮助我们在编译期就发现许多潜在的数据类型错误比如将数字误赋给任务标题。同时ArkUI框架如Component装饰器、State、Prop等与ArkTS语言结合得天衣无缝是实现声明式UI的基石。Stage模型的选择理由Stage模型是HarmonyOS 3.0API 9及以后版本推荐的应用模型。它提供了更清晰的能力组件UIAbility、ExtensionAbility生命周期管理更有利于实现复杂的应用内导航和跨设备迁移。虽然我们的待办列表初期可能只在单设备运行但基于Stage模型构建能为应用未来的扩展比如在平板和手机间同步待办事项提供更好的架构基础。2.2 核心数据结构设计如何定义一条“待办”数据是应用的灵魂。一条待办事项TodoItem至少应包含哪些信息这决定了我们数据模型的设计。// 文件model/TodoItem.ets export class TodoItem { id: string; // 唯一标识用于列表项的key和精准操作 title: string; // 任务标题 completed: boolean; // 完成状态 createdAt: number; // 创建时间戳可用于排序或显示 constructor(title: string) { this.id this.generateId(); // 生成唯一ID this.title title; this.completed false; this.createdAt new Date().getTime(); } private generateId(): string { // 一个简单的唯一ID生成方法实际项目中可使用更健壮的库 return todo_${new Date().getTime()}_${Math.random().toString(36).substr(2, 9)}; } }设计考量唯一标识id这是关键。在列表渲染和进行单项操作如删除、切换状态时依靠索引是不稳定且容易出错的。一个唯一的id能确保我们精准定位到目标数据项。完成状态completed布尔值驱动UI变化的核心状态如文本划线、复选框勾选。时间戳createdAt虽然不是核心功能必需但有了它我们可以轻松实现按创建时间排序或者在UI上显示“今天”、“昨天”等友好时间。好的数据结构要为未来可能的需求留出扩展空间。2.3 应用状态管理方案数据放哪里一个待办列表应用其核心状态就是TodoItem的数组。我们需要决定这个状态由谁持有如何在不同组件间共享和修改。对于这个规模的应用我们采用“自上而下的单向数据流”结合ArkUI状态管理装饰器的方案这足够清晰且高效。状态提升将待办列表数据todoList: ArrayTodoItem定义在最高层级的页面组件例如TodoListPage中并使用State装饰器使其成为响应式状态。状态共享子组件如单个待办项组件TodoListItem通过Prop或Link装饰器接收父组件传递的数据或数据引用。当子组件需要修改数据时如点击完成通过触发父组件传递下来的回调方法来实现从而保证数据修改的源头唯一、可追踪。这种方案避免了早期可能因使用全局变量或过于复杂的状态管理库而引入的混乱。当应用功能变得非常复杂时再考虑引入更专业的状态管理方案如AppStorage或第三方库也不迟。3. 核心功能模块实现与详解有了清晰的设计我们就可以开始动手搭建了。我们将应用拆解为几个核心功能模块逐一实现。3.1 页面布局与静态UI构建首先我们构建应用的主页面骨架。我们将使用Column、Row、List、ListItem等基础容器和组件。// 文件pages/TodoListPage.ets import { TodoItem } from ../model/TodoItem; Entry Component struct TodoListPage { // 响应式状态待办列表 State todoList: ArrayTodoItem []; build() { Column({ space: 0 }) { // 1. 顶部标题栏 Row({ space: 12 }) { Text(我的待办清单) .fontSize(24) .fontWeight(FontWeight.Bold) // 可以在这里添加筛选按钮等 } .width(100%) .padding(20) .backgroundColor(#f1f3f5) // 2. 新增待办输入区域 Row({ space: 12 }) { TextInput({ placeholder: 输入新的待办事项... }) .layoutWeight(1) // 占据剩余空间 .onSubmit((value: string) { // 回车提交事件处理 this.addTodo(value); }) Button(添加) .onClick(() { // 获取TextInput的值这里需要用到ref后续会讲 this.addTodo(this.inputValue); }) } .width(100%) .padding(12) .backgroundColor(Color.White) // 3. 待办事项列表主体 List({ space: 8 }) { ForEach(this.todoList, (item: TodoItem) { ListItem() { // 这里将渲染每一个TodoListItem组件 TodoListItem({ todoItem: item }) } }, (item: TodoItem) item.id) // 关键使用id作为键优化列表渲染 } .layoutWeight(1) // 列表占据剩余所有垂直空间 .width(100%) .backgroundColor(Color.White) // 4. 底部状态栏例如显示未完成数量 Row({ space: 8 }) { Text(还有${this.getActiveCount()}项待完成) .fontSize(14) .fontColor(Color.Gray) } .width(100%) .padding(12) .backgroundColor(#f8f9fa) } .width(100%) .height(100%) .backgroundColor(Color.White) } // 计算未完成数量的方法 private getActiveCount(): number { return this.todoList.filter(item !item.completed).length; } // 添加待办的方法暂未实现具体逻辑 private addTodo(title: string): void { if (!title.trim()) return; // 后续实现 } }要点解析Entry装饰的TodoListPage是应用的入口页面。State todoList这是页面的核心状态。任何对todoList的修改如增、删、改ArkUI框架都会自动检测到并触发UI重新渲染对应的部分。ForEach循环渲染列表这是渲染动态列表的标准方式。特别注意第三个参数(item: TodoItem) item.id它为每个列表项提供了一个唯一的键key。这能帮助ArkUI框架在列表数据变化时如排序、插入、删除高效地识别每个节点进行最小化的UI更新而不是粗暴地重绘整个列表这对性能至关重要。layoutWeight(1)这是一个非常实用的布局属性。它让List和TextInput所在的Row能够根据剩余空间灵活分配尺寸从而实现输入框自适应宽度、列表占满剩余屏幕的效果。3.2 动态交互与状态管理让列表活起来现在UI是静态的我们需要实现交互添加、完成、删除。3.2.1 添加待办事项完善addTodo方法并处理输入框的引用。// 在TodoListPage结构体内 // 使用Link或State绑定输入框的值这里我们用ref来获取组件实例 State inputValue: string ; build() { Column({ space: 0 }) { // ... 顶部标题 ... // 修改输入区域 Row({ space: 12 }) { TextInput({ placeholder: 输入新的待办事项..., text: this.inputValue }) .layoutWeight(1) .onChange((value: string) { this.inputValue value; // 同步输入值到状态 }) .onSubmit((value: string) { this.addTodo(value); }) Button(添加) .onClick(() { this.addTodo(this.inputValue); }) } // ... 列表和底部 ... } } private addTodo(title: string): void { if (!title.trim()) { // 可以添加一个Toast提示 return; } // 1. 创建新的待办项 const newItem new TodoItem(title.trim()); // 2. 更新状态数组。注意必须创建一个新数组来触发UI更新 this.todoList [...this.todoList, newItem]; // 3. 清空输入框 this.inputValue ; }关键技巧this.todoList [...this.todoList, newItem];。为什么不用this.todoList.push(newItem)因为State装饰的数组其内部元素的变化如push,splice不会被ArkUI框架自动观察到。我们必须通过给this.todoList赋予一个全新的数组引用来通知框架状态已变更。这是使用声明式UI状态管理时一个非常重要的原则。3.2.2 实现待办项组件TodoListItem创建子组件来处理每条待办的显示和交互。// 文件components/TodoListItem.ets import { TodoItem } from ../model/TodoItem; Component export struct TodoListItem { // 使用Prop接收父组件传递的单项数据单向同步 Prop todoItem: TodoItem; // 使用Link接收父组件传递的删除函数引用子组件可以调用 Link onDeleteItem: (id: string) void; // 使用Link接收父组件传递的切换状态函数引用 Link onToggleItem: (id: string) void; build() { Row({ space: 12 }) { // 完成状态复选框 Checkbox() .select(this.todoItem.completed) .onChange((checked: boolean) { // 状态改变时通知父组件 this.onToggleItem(this.todoItem.id); }) // 任务标题文本 Text(this.todoItem.title) .fontSize(18) .textAlign(TextAlign.Start) .layoutWeight(1) // 文本占满剩余水平空间 .decoration({ type: this.todoItem.completed ? TextDecorationType.LineThrough : TextDecorationType.None }) // 完成时划线 .fontColor(this.todoItem.completed ? Color.Gray : Color.Black) // 删除按钮 Button(删除, { type: ButtonType.Normal }) .fontSize(14) .onClick(() { // 点击删除时通知父组件 this.onDeleteItem(this.todoItem.id); }) } .width(100%) .padding(12) .borderRadius(8) .backgroundColor(Color.White) .shadow({ radius: 2, color: #eee, offsetX: 0, offsetY: 1 }) } }3.2.3 在父页面中集成并使用子组件回到TodoListPage我们需要将操作函数传递给子组件。// 在TodoListPage结构体内 build() { List({ space: 8 }) { ForEach(this.todoList, (item: TodoItem) { ListItem() { TodoListItem({ todoItem: item, // 传递数据 onDeleteItem: this.deleteTodo.bind(this), // 传递删除方法 onToggleItem: this.toggleTodo.bind(this) // 传递切换方法 }) } }, (item: TodoItem) item.id) } } // 删除待办 private deleteTodo(id: string): void { this.todoList this.todoList.filter(item item.id ! id); } // 切换待办完成状态 private toggleTodo(id: string): void { this.todoList this.todoList.map(item { if (item.id id) { // 返回一个新对象而不是修改原对象 return { ...item, completed: !item.completed }; } return item; }); }设计模式解析PropvsLinkProp是单向同步父组件变子组件变但子组件内部修改Prop变量不会影响父组件。Link是双向同步任何一方的修改都会同步到另一方。这里todoItem用Prop因为子组件不应该直接修改它而是通过回调函数而回调函数引用onDeleteItem和onToggleItem用Link确保子组件能调用到最新的函数。数据不可变性注意toggleTodo方法中我们使用了map返回一个新数组并且对于要修改的项使用了对象扩展运算符{ ...item, completed: !item.completed }创建了一个新对象。这同样是遵循状态更新的原则确保引用变化能被框架捕获。3.3 数据持久化让待办列表记住状态应用重启后待办列表不能消失。我们需要将数据保存到设备本地。HarmonyOS提供了多种数据持久化方案对于待办列表这种结构化的、数据量不大的场景关系型数据库RDB或轻量级偏好数据库Preferences都是不错的选择。这里我们使用更简单的Preferences来演示。Preferences以键值对形式存储适合存储配置或简单的结构化数据需序列化。3.3.1 创建数据管理工具类// 文件utils/PreferencesUtil.ets import preferences from ohos.data.preferences; const PREFERENCES_NAME my_todo_app; const KEY_TODO_LIST todo_list; export class PreferencesUtil { // 获取Preferences实例异步 static async getPreferences(context: common.UIAbilityContext): Promisepreferences.Preferences { try { return await preferences.getPreferences(context, PREFERENCES_NAME); } catch (err) { console.error(Failed to get preferences. Code: ${err.code}, message: ${err.message}); throw err; } } // 保存待办列表 static async saveTodoList(context: common.UIAbilityContext, todoList: ArrayTodoItem): Promisevoid { try { const prefs await this.getPreferences(context); const listJson JSON.stringify(todoList); // 序列化为JSON字符串 await prefs.put(KEY_TODO_LIST, listJson); await prefs.flush(); // 提交更改 } catch (err) { console.error(Failed to save todo list. Code: ${err.code}, message: ${err.message}); } } // 加载待办列表 static async loadTodoList(context: common.UIAbilityContext): PromiseArrayTodoItem { try { const prefs await this.getPreferences(context); const listJson await prefs.get(KEY_TODO_LIST, []); // 默认值空数组JSON return JSON.parse(listJson as string); // 反序列化 // 注意这里反序列化出来的对象会丢失TodoItem类的方法如果需要可以再映射一次 } catch (err) { console.error(Failed to load todo list. Code: ${err.code}, message: ${err.message}); return []; } } }3.3.2 在UIAbility中集成数据加载与保存数据持久化通常与UIAbility的生命周期挂钩。我们在应用启动时加载数据在应用退出或数据变更时保存。// 文件entryability/EntryAbility.ets import { PreferencesUtil } from ../utils/PreferencesUtil; import { TodoItem } from ../model/TodoItem; export default class EntryAbility extends UIAbility { // 应用创建时可以初始化一些资源 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { console.log(EntryAbility onCreate); } // 当UIAbility实例销毁时保存数据 onDestroy(): void { console.log(EntryAbility onDestroy); // 注意这里需要获取到页面存储的数据通常需要通过全局状态或事件通知来获取。 // 更常见的做法是在Page的aboutToDisappear生命周期或每次数据变更时保存。 } // 窗口创建时可以在这里加载数据并传递给页面 onWindowStageCreate(windowStage: window.WindowStage): void { console.log(EntryAbility onWindowStageCreate); // 加载持久化的数据 this.loadDataAndStart(windowStage); } private async loadDataAndStart(windowStage: window.WindowStage): Promisevoid { try { const savedList: Arrayany await PreferencesUtil.loadTodoList(this.context); // 将加载的纯对象数组转换为TodoItem实例数组可选为了保留类方法 const todoList: ArrayTodoItem savedList.map(item { const todo new TodoItem(item.title); todo.id item.id || todo.id; // 使用保存的id todo.completed item.completed; todo.createdAt item.createdAt; return todo; }); // 将数据通过want的参数传递给页面方式一 const want { parameters: { initialTodoList: todoList } }; windowStage.loadContent(pages/TodoListPage, (err, data) { if (err.code) { console.error(Failed to load the content. Code: ${err.code}, message: ${err.message}); return; } console.log(Succeeded in loading the content.); }, want); // 传递want参数 } catch (err) { console.error(Failed to load data. Code: ${err.code}, message: ${err.message}); // 即使加载失败也启动页面 windowStage.loadContent(pages/TodoListPage, (err, data) { // ... handle error }); } } }3.3.3 在页面中接收初始数据并实现自动保存修改TodoListPage使其能接收初始数据并在数据变化时自动保存。// 文件pages/TodoListPage.ets import { PreferencesUtil } from ../utils/PreferencesUtil; import common from ohos.app.ability.common; Entry Component struct TodoListPage { State todoList: ArrayTodoItem []; // 通过StorageLink或AppStorage获取UIAbilityContext这里使用一个简单的方法需在aboutToAppear中赋值 private context: common.UIAbilityContext | undefined undefined; aboutToAppear() { // 获取UIAbility的上下文用于数据持久化 this.context getContext(this) as common.UIAbilityContext; // 从启动参数中获取初始数据 const params getContext(this)?.startAbilityParameter?.parameters; if (params params.initialTodoList) { this.todoList params.initialTodoList; } } // 修改所有会改变todoList的方法在改变后自动保存 private async addTodo(title: string): Promisevoid { if (!title.trim()) return; const newItem new TodoItem(title.trim()); this.todoList [...this.todoList, newItem]; await this.saveData(); } private async deleteTodo(id: string): Promisevoid { this.todoList this.todoList.filter(item item.id ! id); await this.saveData(); } private async toggleTodo(id: string): Promisevoid { this.todoList this.todoList.map(item { if (item.id id) { return { ...item, completed: !item.completed }; } return item; }); await this.saveData(); } // 统一的保存方法 private async saveData(): Promisevoid { if (this.context) { await PreferencesUtil.saveTodoList(this.context, this.todoList); } } }注意在实际开发中获取UIAbilityContext的方式可能因API版本和具体场景略有不同。上述getContext(this)是一种方式也可能需要通过AppStorage或LocalStorage来传递。这里为了示例清晰做了简化。更健壮的做法可能是使用一个全局的数据存储管理器DataStore它持有context并统一处理所有持久化逻辑页面组件只与这个管理器交互。3.4 功能增强筛选与数据统计一个基本的待办列表已经完成。我们可以再添加一些常见功能比如筛选全部/未完成/已完成和更详细的数据统计。3.4.1 添加筛选功能在TodoListPage中增加筛选状态和对应的UI。// 在TodoListPage结构体内 // 定义筛选类型 private FilterType { ALL: all, ACTIVE: active, COMPLETED: completed }; State currentFilter: string this.FilterType.ALL; // 当前筛选状态 // 计算当前应该显示的列表 private get filteredList(): ArrayTodoItem { switch (this.currentFilter) { case this.FilterType.ACTIVE: return this.todoList.filter(item !item.completed); case this.FilterType.COMPLETED: return this.todoList.filter(item item.completed); case this.FilterType.ALL: default: return this.todoList; } } build() { Column({ space: 0 }) { // ... 顶部标题 ... // ... 输入区域 ... // 在列表上方添加筛选器 Row({ space: 20 }) { ForEach([全部, 未完成, 已完成], (filterText: string, index?: number) { let filterValue [this.FilterType.ALL, this.FilterType.ACTIVE, this.FilterType.COMPLETED][index]; Button(filterText, { type: ButtonType.Capsule }) .stateEffect(this.currentFilter filterValue) // 高亮当前选中项 .backgroundColor(this.currentFilter filterValue ? #007dff : #f0f0f0) .fontColor(this.currentFilter filterValue ? Color.White : Color.Black) .onClick(() { this.currentFilter filterValue; }) }) } .width(100%) .padding(12) .justifyContent(FlexAlign.Center) // 修改List的数据源为filteredList List({ space: 8 }) { ForEach(this.filteredList, (item: TodoItem) { ListItem() { TodoListItem({ todoItem: item, onDeleteItem: this.deleteTodo.bind(this), onToggleItem: this.toggleTodo.bind(this) }) } }, (item: TodoItem) item.id) } .layoutWeight(1) // ... 底部状态栏 ... } }3.4.2 增强底部状态栏修改底部状态栏显示更丰富的信息。// 在TodoListPage的build方法中修改底部Row Row({ space: 20 }) { Text(${this.getActiveCount()} 项待办) .fontSize(14) if (this.todoList.length this.getActiveCount()) { Text(${this.todoList.length - this.getActiveCount()} 项已完成) .fontSize(14) .fontColor(Color.Gray) } if (this.todoList.length 0) { Button(清除已完成, { type: ButtonType.Normal }) .fontSize(14) .onClick(() { this.clearCompleted(); }) } } .width(100%) .padding(12) .justifyContent(FlexAlign.SpaceBetween) // 左右分布 .backgroundColor(#f8f9fa) // 新增清除已完成的方法 private async clearCompleted(): Promisevoid { this.todoList this.todoList.filter(item !item.completed); await this.saveData(); }4. 常见问题、调试技巧与性能优化在开发过程中你肯定会遇到各种问题。这里记录了一些典型问题的排查思路和优化建议。4.1 常见问题与解决方案速查表问题现象可能原因解决方案与排查步骤列表更新后UI不刷新1. 直接修改了State数组的内部元素如push,splice。2. 修改了Prop或State对象的属性但对象引用本身未变。1. 确保总是为State变量赋予新的引用新数组、新对象。2. 使用扩展运算符[...array]、map、filter或Object.assign({}, obj)来创建新引用。ForEach渲染错误或控制台警告未提供或提供了不稳定的key生成函数。确保ForEach的第三个参数返回列表中每个项唯一且稳定的标识符如item.id。不要使用索引index作为key除非列表是静态的。点击事件无响应1. 组件未设置onClick。2. 事件被父组件的某个区域遮挡。3. 组件enabled状态为false。1. 检查事件绑定语法。2. 检查布局确保可点击区域有足够大小且未被覆盖。使用调试工具的组件树查看。3. 检查组件状态。保存到Preferences的数据读取为空1.flush()未调用或调用失败。2. 序列化/反序列化出错。3.key不一致。1. 确保put操作后调用了await prefs.flush()。2. 使用try...catch包裹并打印错误日志。检查存储的JSON字符串格式是否正确。3. 检查保存和读取时使用的PREFERENCES_NAME和KEY_TODO_LIST是否完全一致。页面样式错乱1. 宽度/高度未设置或设置不当。2. 布局容器嵌套或属性冲突。3. 使用了不兼容的样式组合。1. 多用%、vp等相对单位少用固定像素。使用.width(100%)、.layoutWeight(1)等。2. 使用DevEco Studio的预览器或模拟器实时查看并逐个组件检查样式。从外层容器向内层排查。应用启动白屏或崩溃1. UIAbility或Page的aboutToAppear中有同步阻塞操作。2. 加载了过大的初始数据。3. 代码存在语法或运行时错误。1. 将耗时的初始化操作如网络请求、大量数据读取放在异步任务中或使用aboutToAppear的异步版本如果支持。2. 查看Log窗口HiLog中的错误信息这是最主要的调试手段。4.2 调试技巧与心得善用HiLogconsole.log在HarmonyOS中对应的是hilog。在DevEco Studio的Log窗口你可以过滤查看应用日志。这是定位运行时错误、跟踪数据流最直接的方法。建议对关键的函数入口、状态变更处添加日志。import hilog from ohos.hilog; hilog.info(0x0000, MyTodoTag, 添加待办标题%{public}s, title);实时预览与热重载DevEco Studio的预览器Previewer非常强大支持大部分UI和交互的实时预览。结合“Enable Hot Reload”功能修改代码后能快速看到效果极大提升开发效率。组件检查与布局调试在预览器或模拟器中运行应用可以使用“组件树”视图来检查UI组件的层级、属性和样式对于排查布局问题非常有帮助。性能关注点列表渲染确保ForEach的key正确设置这是列表性能的基石。对于超长列表考虑使用LazyForEach进行懒加载。状态管理避免将不必要的状态提升到过高层级这会导致无关的UI组件一起重绘。合理使用State,Prop,Link,Provide/Consume等装饰器将状态管理在合适的范围内。图片资源如果待办事项包含图片注意优化图片尺寸并使用合适的加载方式。4.3 项目扩展思考这个基础版本已经可用但还有很多可以深化和扩展的方向UI美化引入更丰富的ArkUI组件和动画例如任务完成时的划掉动画、删除时的滑动消失动画。多设备协同利用HarmonyOS的分布式能力将待办列表数据库设置为“分布式数据库”实现手机、平板、智慧屏等多设备间的数据自动同步。通知与提醒为待办事项添加时间提醒功能使用ohos.notification模块在指定时间触发系统通知。数据备份与导出增加将待办列表导出为JSON或文本文件的功能或备份到云盘。分类与标签为待办事项增加分类或标签功能实现更复杂的信息组织。开发这个待办列表的过程实际上是一个深入学习HarmonyOS应用开发核心概念的绝佳路径。从声明式UI的构建、状态管理的数据流、到本地持久化的方案选择每一步都踩在了HarmonyOS应用开发的要点上。当你把这个应用跑起来并且能稳定地添加、完成、删除任务时你对ArkTS和ArkUI的理解就已经上了一个坚实的台阶。接下来无论是开发更复杂的应用还是探索分布式特性你都有了扎实的根基。