手摸手教你HarmonyOS鸿蒙应用开发

174 阅读11分钟

一、项目规划与准备

1.1 应用功能设计

在开始编码之前,我们需要先明确应用的功能和界面设计。本教程将带领大家开发一个实用的"个人待办事项"应用,具备以下核心功能:

核心功能

  • 添加新待办事项(支持标题和内容)
  • 标记待办事项完成状态
  • 删除不需要的待办事项
  • 数据本地持久化存储
  • 待办事项分类(工作/生活/学习)

界面规划

  1. 首页:待办事项列表,显示所有待办事项
  2. 添加页:添加新待办事项的表单界面
  3. 详情页:查看和编辑待办事项详情

技术选型

  • 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 NameTodoApp(项目名称)
  • Package Namecom.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 应用签名

鸿蒙应用需要签名后才能安装到真机或发布到应用市场:

签名步骤

  1. 在DevEco Studio中,点击"Build > Generate Key and CSR"生成密钥和证书请求
  2. 在华为开发者联盟申请应用证书
  3. 点击"Build > Signing Config"配置签名信息
  4. 选择"Build > Build HAP(s)"生成签名后的HAP包

6.3 应用上架准备

准备将应用上架到华为应用市场:

上架前检查清单

  •  应用功能完整且无崩溃
  •  应用图标和截图符合要求
  •  隐私政策和用户协议已准备
  •  应用性能和稳定性良好
  •  已通过华为应用市场审核指南检查

上架流程

  1. 在华为应用市场创建应用
  2. 上传签名后的HAP包
  3. 填写应用信息和上传截图
  4. 提交审核
  5. 审核通过后发布应用

七、总结与下一步学习

7.1 项目回顾

在本教程中,我们从零开始开发了一个功能完整的待办事项应用,学习了以下内容:

  • 鸿蒙应用项目创建和目录结构
  • 声明式UI开发和布局设计
  • 自定义组件开发
  • 状态管理和数据绑定
  • 本地数据持久化存储
  • 页面路由和导航
  • 应用调试和优化
  • 应用打包和发布准备

通过这个项目,你已经掌握了鸿蒙应用开发的基础知识和技能。

7.2 下一步学习建议

为了进一步提升鸿蒙开发技能,建议学习以下内容:

进阶技术

  • 分布式能力开发
  • 多端适配和响应式布局
  • 动画和交互效果
  • 网络请求和数据解析
  • 单元测试和UI测试

实战项目

  • 天气应用:学习网络请求和数据可视化
  • 新闻应用:学习列表优化和富文本展示
  • 社交应用:学习分布式能力和实时通信

社区资源

码牛教育官方的动态 - 哔哩哔哩

  • 鸿蒙开发者文档
  • DevEco Studio教程
  • 鸿蒙开发视频课程

希望本教程能够帮助你快速入门鸿蒙应用开发。记住,编程学习最关键的是持续实践,不断构建项目来巩固所学知识。祝你在鸿蒙开发之路上取得成功!