一、项目规划与准备
1.1 应用功能设计
在开始编码之前,我们需要先明确应用的功能和界面设计。本教程将带领大家开发一个实用的"个人待办事项"应用,具备以下核心功能:
核心功能:
- 添加新待办事项(支持标题和内容)
- 标记待办事项完成状态
- 删除不需要的待办事项
- 数据本地持久化存储
- 待办事项分类(工作/生活/学习)
界面规划:
- 首页:待办事项列表,显示所有待办事项
- 添加页:添加新待办事项的表单界面
- 详情页:查看和编辑待办事项详情
技术选型:
- UI框架:ArkUI(声明式UI)
- 状态管理:@State、@Prop、@Link
- 本地存储:Preferences(轻量级键值对存储)
- 路由管理:鸿蒙路由模块
1.2 开发环境准备
在开始开发前,请确保你的开发环境已准备就绪:
环境检查清单:
- DevEco Studio 5.0+已安装
- HarmonyOS SDK 5.0+已配置
- 华为开发者账号已注册并登录
- 模拟器或真机已配置可用
开发工具:
- DevEco Studio:鸿蒙应用开发IDE
- 鸿蒙模拟器:用于应用调试
- 代码编辑器:DevEco Studio内置或其他编辑器
如果你还没有准备好开发环境,可以参考我之前的文章《鸿蒙开发环境搭建完全指南》进行配置。
二、项目创建与目录结构
2.1 创建新项目
现在,让我们动手创建项目:
Step 1:启动DevEco Studio
打开DevEco Studio,点击"Create Project"按钮开始创建新项目。
Step 2:选择项目模板
在模板选择界面,我们选择"Empty Ability"模板,这是创建鸿蒙应用的基础模板。点击"Next"进入下一步。
Step 3:配置项目信息
在项目配置界面,填写以下信息:
- Project Name:
TodoApp(项目名称) - Package Name:
com.example.todo(反向域名格式) - Save Location:选择合适的项目保存路径
- Compile SDK:选择最新的SDK版本(API 12+)
- Device Type:选择"Phone"(我们以手机应用为例)
- Language:选择"ArkTS"
填写完成后点击"Finish",等待项目创建完成。
2.2 项目目录结构解析
项目创建完成后,我们来了解一下鸿蒙应用的目录结构:
TodoApp/
├── AppScope/ # 应用全局配置
│ └── resources/ # 应用全局资源
├── entry/ # 主应用模块
│ ├── src/main/ets/ # 源代码目录
│ │ ├── entryability/ # 应用入口
│ │ │ └── EntryAbility.ts # 应用生命周期管理
│ ├── pages/ # 页面目录
│ │ ├── Index.ets # 应用首页
│ │ ├── AddTodo.ets # 添加待办页面
│ │ └── TodoDetail.ets # 待办详情页面
│ ├── components/ # 自定义组件目录
│ ├── models/ # 数据模型目录
│ └── services/ # 业务服务目录
│ ├── src/main/resources/ # 模块资源目录
│ └── module.json5 # 模块配置文件
└── build-profile.json5 # 项目构建配置
核心目录说明:
- pages:存放应用的页面组件
- components:存放可复用的自定义组件
- models:存放数据模型定义
- services:存放业务逻辑和服务
三、UI界面实现
3.1 首页布局实现
首页将展示待办事项列表,让我们一步步实现它:
Step 1:设计页面结构
首页主要包含以下元素:
- 页面标题
- 添加按钮(跳转添加页)
- 待办事项列表
- 空状态提示(当没有待办事项时显示)
Step 2:编写页面代码
打开pages/Index.ets文件,替换为以下代码:
@Entry
@Component
struct TodoListPage {
// 定义状态变量存储待办事项
@State todos: TodoItem[] = []
@State isLoading: boolean = true
build() {
Column() {
// 页面标题栏
Row() {
Text("我的待办事项")
.fontSize(22)
.fontWeight(FontWeight.Bold)
// 添加按钮
Button() {
Image($r("app.media.ic_add"))
.width(24)
.height(24)
}
.width(40)
.height(40)
.backgroundColor("#007AFF")
.borderRadius(20)
.onClick(() => {
// 跳转到添加页面
router.pushUrl({ url: "pages/AddTodo" })
})
}
.width("100%")
.padding(15)
.justifyContent(FlexAlign.SpaceBetween)
// 待办事项列表
if (this.isLoading) {
// 加载状态
Progress({ type: ProgressType.Circular })
.width(40)
.height(40)
.margin(20)
} else if (this.todos.length === 0) {
// 空状态
Column() {
Image($r("app.media.ic_empty"))
.width(120)
.height(120)
Text("暂无待办事项")
.fontSize(16)
.color("#999")
.margin(10)
}
.flexGrow(1)
.justifyContent(FlexAlign.Center)
} else {
// 待办列表
List({ space: 8 }) {
ForEach(this.todos, (item) => {
ListItem() {
TodoItemComponent({
todo: item,
onStatusChange: (isCompleted) => {
// 状态变化回调
this.updateTodoStatus(item.id, isCompleted)
},
onDelete: () => {
// 删除回调
this.deleteTodo(item.id)
}
})
}
})
}
.padding(10)
.flexGrow(1)
}
}
.width("100%")
.height("100%")
.backgroundColor("#F5F5F5")
}
// 页面加载时获取数据
aboutToAppear() {
this.loadTodos()
}
// 从本地存储加载待办事项
async loadTodos() {
// 模拟加载延迟
setTimeout(async () => {
// 实际项目中这里应该从本地存储加载数据
this.todos = [ { id: 1, title: "学习鸿蒙开发", content: "完成手摸手教程", isCompleted: false, category: "学习", createTime: new Date() }, { id: 2, title: "购买 groceries", content: "牛奶、鸡蛋、面包", isCompleted: true, category: "生活", createTime: new Date() } ]
this.isLoading = false
}, 1000)
}
// 更新待办事项状态
updateTodoStatus(id: number, isCompleted: boolean) {
this.todos = this.todos.map(item =>
item.id === id ? { ...item, isCompleted } : item
)
// 实际项目中这里应该保存到本地存储
}
// 删除待办事项
deleteTodo(id: number) {
this.todos = this.todos.filter(item => item.id !== id)
// 实际项目中这里应该从本地存储删除
}
}
代码解析:
- 我们使用
@State装饰器定义了待办事项数组和加载状态变量 - 在
build()方法中描述了页面UI结构 - 使用条件渲染实现了加载状态、空状态和列表状态的切换
- 添加了页面加载时的数据加载逻辑
- 实现了待办事项状态更新和删除的方法
3.2 自定义待办项组件
为了代码复用和结构清晰,我们将待办事项项封装为自定义组件:
Step 1:创建组件文件
在components目录下创建TodoItemComponent.ets文件。
Step 2:编写组件代码
@Component
export struct TodoItemComponent {
@Prop todo: TodoItem
@Prop onStatusChange: (isCompleted: boolean) => void
@Prop onDelete: () => void
build() {
Row() {
// 复选框
Checkbox()
.selected(this.todo.isCompleted)
.onChange((isSelected) => {
this.onStatusChange(isSelected)
})
// 待办内容
Column() {
Text(this.todo.title)
.fontSize(16)
.textDecoration(this.todo.isCompleted
? TextDecoration.LineThrough
: TextDecoration.None)
Text(this.todo.content)
.fontSize(14)
.color("#666")
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.flexGrow(1)
.margin({ left: 10 })
// 删除按钮
Button() {
Image($r("app.media.ic_delete"))
.width(20)
.height(20)
}
.width(36)
.height(36)
.type(ButtonType.Circle)
.backgroundColor("#F53F3F")
.onClick(() => {
this.onDelete()
})
}
.width("100%")
.padding(12)
.backgroundColor(Color.White)
.borderRadius(10)
}
}
组件设计思路:
- 使用
@Prop接收父组件传递的属性和回调函数 - 布局采用Row容器,水平排列复选框、内容和删除按钮
- 根据待办事项完成状态显示不同样式(删除线)
- 点击复选框触发状态变更回调
- 点击删除按钮触发删除回调
3.3 添加页面实现
现在让我们实现添加待办事项的页面:
Step 1:创建页面文件
在pages目录下创建AddTodo.ets文件。
Step 2:编写页面代码
@Entry
@Component
struct AddTodoPage {
@State title: string = ""
@State content: string = ""
@State category: string = "工作"
private categories: string[] = ["工作", "生活", "学习"]
build() {
Column() {
// 标题栏
Row() {
Button() {
Image($r("app.media.ic_back"))
.width(24)
.height(24)
}
.width(40)
.height(40)
.backgroundColor(Color.Transparent)
.onClick(() => {
router.back()
})
Text("添加待办事项")
.fontSize(20)
.fontWeight(FontWeight.Bold)
Blank()
}
.width("100%")
.padding(15)
// 输入表单
Column({ space: 15 }) {
// 标题输入
TextInput({ placeholder: "请输入待办标题" })
.width("100%")
.height(48)
.backgroundColor(Color.White)
.padding(15)
.borderRadius(8)
.onChange((value) => {
this.title = value
})
// 内容输入
TextArea({ placeholder: "请输入待办内容" })
.width("100%")
.height(120)
.backgroundColor(Color.White)
.padding(15)
.borderRadius(8)
.onChange((value) => {
this.content = value
})
// 分类选择
Row() {
Text("分类:")
.fontSize(16)
Blank()
Dropdown() {
ForEach(this.categories, (item) => {
Text(item)
.onClick(() => {
this.category = item
})
})
}
.value(this.category)
.width(120)
}
.width("100%")
// 保存按钮
Button("保存")
.width("100%")
.height(48)
.backgroundColor("#007AFF")
.fontSize(16)
.borderRadius(8)
.onClick(() => {
this.saveTodo()
})
}
.width("100%")
.padding(15)
Blank()
}
.width("100%")
.height("100%")
.backgroundColor("#F5F5F5")
}
// 保存待办事项
saveTodo() {
if (!this.title.trim()) {
prompt.showToast({ message: "请输入待办标题" })
return
}
// 创建新待办事项
const newTodo = {
id: Date.now(),
title: this.title,
content: this.content,
isCompleted: false,
category: this.category,
createTime: new Date()
}
// 实际项目中这里应该保存到本地存储
// 返回首页
router.back()
}
}
页面功能说明:
- 实现了待办事项标题和内容的输入
- 添加了分类选择功能
- 实现了表单验证和保存逻辑
- 添加了返回首页功能
四、数据持久化实现
4.1 封装存储服务
为了统一管理数据存储操作,我们封装一个存储服务:
Step 1:创建存储服务
在services目录下创建StorageService.ts文件:
import preferences from '@ohos.data.preferences';
export class StorageService {
private static instance: StorageService;
private preferences: preferences.Preferences | null = null;
private readonly STORE_NAME = 'todo_storage';
private readonly TODO_KEY = 'todos';
// 单例模式
static getInstance(): StorageService {
if (!StorageService.instance) {
StorageService.instance = new StorageService();
}
return StorageService.instance;
}
// 初始化存储
async init(context: any): Promise<void> {
if (!this.preferences) {
this.preferences = await preferences.getPreferences(context, this.STORE_NAME);
}
}
// 保存待办事项列表
async saveTodos(todos: TodoItem[]): Promise<boolean> {
try {
if (!this.preferences) return false;
await this.preferences.put(this.TODO_KEY, JSON.stringify(todos));
await this.preferences.flush();
return true;
} catch (error) {
console.error('保存待办事项失败', error);
return false;
}
}
// 获取待办事项列表
async getTodos(): Promise<TodoItem[]> {
try {
if (!this.preferences) return [];
const todosJson = await this.preferences.get(this.TODO_KEY, '[]');
return JSON.parse(todosJson);
} catch (error) {
console.error('获取待办事项失败', error);
return [];
}
}
}
4.2 在应用中使用存储服务
现在我们需要修改之前的代码,集成存储服务:
Step 1:在Ability中初始化存储服务
修改entryability/EntryAbility.ts文件:
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import { StorageService } from '../services/StorageService';
export default class EntryAbility extends UIAbility {
async onCreate(want, launchParam) {
// 初始化存储服务
await StorageService.getInstance().init(this.context);
}
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err) => {
if (err) {
console.error('加载页面失败', err);
}
});
}
}
Step 2:修改首页加载和保存逻辑
更新首页代码,使用存储服务加载和保存数据:
// 从本地存储加载待办事项
async loadTodos() {
try {
this.isLoading = true;
const storage = StorageService.getInstance();
this.todos = await storage.getTodos();
} catch (error) {
console.error('加载待办事项失败', error);
// 使用默认数据
this.todos = [/* 默认数据 */];
} finally {
this.isLoading = false;
}
}
// 更新待办事项状态
async updateTodoStatus(id: number, isCompleted: boolean) {
this.todos = this.todos.map(item =>
item.id === id ? { ...item, isCompleted } : item
);
try {
const storage = StorageService.getInstance();
await storage.saveTodos(this.todos);
} catch (error) {
console.error('保存待办事项失败', error);
prompt.showToast({ message: '保存失败,请重试' });
}
}
Step 3:修改添加页面保存逻辑
更新添加页面代码,使用存储服务保存新待办事项:
// 保存待办事项
async saveTodo() {
if (!this.title.trim()) {
prompt.showToast({ message: "请输入待办标题" });
return;
}
// 创建新待办事项
const newTodo = {
id: Date.now(),
title: this.title,
content: this.content,
isCompleted: false,
category: this.category,
createTime: new Date().toISOString()
};
try {
const storage = StorageService.getInstance();
const todos = await storage.getTodos();
todos.push(newTodo);
await storage.saveTodos(todos);
// 返回首页
router.back();
prompt.showToast({ message: "保存成功" });
} catch (error) {
console.error('保存待办事项失败', error);
prompt.showToast({ message: '保存失败,请重试' });
}
}
五、调试与优化
5.1 常见问题排查
在开发过程中,你可能会遇到以下问题,这里提供解决方案供参考:
问题1:页面跳转失败
-
可能原因:页面未在
main_pages.json中注册 -
解决方案:确保所有页面都在
main_pages.json中注册:{ "src": [ "pages/Index", "pages/AddTodo", "pages/TodoDetail" ] }
问题2:存储服务初始化失败
- 可能原因:上下文传递错误或权限问题
- 解决方案:确保在Ability的onCreate中正确初始化存储服务,并申请必要的权限
问题3:UI不更新
-
可能原因:直接修改数组元素而未创建新数组
-
解决方案:始终创建新数组来触发状态更新:
// 错误方式 this.todos[0].isCompleted = true; // 正确方式 this.todos = this.todos.map(item => item.id === 0 ? { ...item, isCompleted: true } : item );
5.2 性能优化建议
为了提升应用性能和用户体验,建议进行以下优化:
列表优化:
- 使用
LazyForEach替代ForEach渲染长列表 - 设置合理的
cachedCount预加载数量
图片优化:
- 使用适当分辨率的图片
- 实现图片懒加载
启动优化:
- 延迟初始化非关键组件
- 异步加载非首屏数据
内存管理:
- 及时取消订阅和事件监听
- 避免内存泄漏
六、打包与发布准备
6.1 应用配置完善
在打包发布前,需要完善应用配置:
应用图标配置:
替换resources/base/media目录下的图标文件,确保应用图标符合规范。
应用名称和描述:
修改module.json5文件,完善应用名称和描述:
"app": {
"bundleName": "com.example.todo",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
6.2 应用签名
鸿蒙应用需要签名后才能安装到真机或发布到应用市场:
签名步骤:
- 在DevEco Studio中,点击"Build > Generate Key and CSR"生成密钥和证书请求
- 在华为开发者联盟申请应用证书
- 点击"Build > Signing Config"配置签名信息
- 选择"Build > Build HAP(s)"生成签名后的HAP包
6.3 应用上架准备
准备将应用上架到华为应用市场:
上架前检查清单:
- 应用功能完整且无崩溃
- 应用图标和截图符合要求
- 隐私政策和用户协议已准备
- 应用性能和稳定性良好
- 已通过华为应用市场审核指南检查
上架流程:
- 在华为应用市场创建应用
- 上传签名后的HAP包
- 填写应用信息和上传截图
- 提交审核
- 审核通过后发布应用
七、总结与下一步学习
7.1 项目回顾
在本教程中,我们从零开始开发了一个功能完整的待办事项应用,学习了以下内容:
- 鸿蒙应用项目创建和目录结构
- 声明式UI开发和布局设计
- 自定义组件开发
- 状态管理和数据绑定
- 本地数据持久化存储
- 页面路由和导航
- 应用调试和优化
- 应用打包和发布准备
通过这个项目,你已经掌握了鸿蒙应用开发的基础知识和技能。
7.2 下一步学习建议
为了进一步提升鸿蒙开发技能,建议学习以下内容:
进阶技术:
- 分布式能力开发
- 多端适配和响应式布局
- 动画和交互效果
- 网络请求和数据解析
- 单元测试和UI测试
实战项目:
- 天气应用:学习网络请求和数据可视化
- 新闻应用:学习列表优化和富文本展示
- 社交应用:学习分布式能力和实时通信
社区资源:
- 鸿蒙开发者文档
- DevEco Studio教程
- 鸿蒙开发视频课程
希望本教程能够帮助你快速入门鸿蒙应用开发。记住,编程学习最关键的是持续实践,不断构建项目来巩固所学知识。祝你在鸿蒙开发之路上取得成功!