文章目录
-
- [一、鸿蒙开发与 ArkTS 语言概述](#一、鸿蒙开发与 ArkTS 语言概述)
-
- [1.1 鸿蒙系统简介](#1.1 鸿蒙系统简介)
- [1.2 ArkTS 语言的地位与特点](#1.2 ArkTS 语言的地位与特点)
- [二、ArkTS 基础语法详解](#二、ArkTS 基础语法详解)
-
- [2.1 变量与数据类型](#2.1 变量与数据类型)
- [2.2 函数定义与调用](#2.2 函数定义与调用)
- [2.3 控制流语句](#2.3 控制流语句)
- [2.4 类与面向对象编程](#2.4 类与面向对象编程)
- [三、基于 ArkTS 的 UI 开发](#三、基于 ArkTS 的 UI 开发)
-
- [3.1 声明式 UI 概念与优势](#3.1 声明式 UI 概念与优势)
- [3.2 常用 UI 组件](#3.2 常用 UI 组件)
- [3.3 布局容器的使用](#3.3 布局容器的使用)
- 四、状态管理在鸿蒙应用中的实践
-
- [4.1 状态管理概念](#4.1 状态管理概念)
- [4.2 常用状态装饰器](#4.2 常用状态装饰器)
- [4.3 状态管理最佳实践](#4.3 状态管理最佳实践)
- 五、鸿蒙应用开发实战:构建简单应用
-
- [5.1 开发环境搭建](#5.1 开发环境搭建)
- [5.2 项目结构解析](#5.2 项目结构解析)
- [5.3 资源管理基础](#5.3 资源管理基础)
- [5.4 应用开发步骤与代码实现](#5.4 应用开发步骤与代码实现)
一、鸿蒙开发与 ArkTS 语言概述

1.1 鸿蒙系统简介
鸿蒙系统(HarmonyOS)是华为公司自主研发的一款面向全场景的分布式操作系统,其设计初衷是为了解决多设备之间的互联互通和协同工作问题,旨在为用户提供更加智能、便捷的全场景体验。
鸿蒙系统的分布式特性是其核心优势之一。它通过分布式软总线、分布式数据管理、分布式任务调度等技术,实现了不同设备之间的无缝连接和协同工作,打破了设备之间的界限,让多个设备可以像一个设备一样协同工作,形成一个超级终端。例如,在智能家居场景中,用户可以通过手机控制智能音箱播放音乐,同时将手机上的视频投屏到智能电视上播放,实现多设备之间的协同工作。
鸿蒙系统的应用场景非常广泛,涵盖了智能家居、智慧办公、智慧出行、影音娱乐、运动健康等多个领域。在智能家居领域,鸿蒙系统可以实现智能家电之间的互联互通,用户可以通过手机或智能音箱等设备控制家电的开关、调节温度等;在智慧办公领域,鸿蒙系统可以实现多设备之间的文件共享、协同编辑等功能,提高办公效率;在智慧出行领域,鸿蒙系统可以与车载系统集成,实现车辆的智能控制和信息交互;在影音娱乐领域,鸿蒙系统可以提供更加流畅的视频播放和音乐播放体验;在运动健康领域,鸿蒙系统可以与智能穿戴设备集成,实现运动数据的实时监测和分析。
1.2 ArkTS 语言的地位与特点
ArkTS(Ark TypeScript)是鸿蒙开发的主力编程语言,它是在 TypeScript 的基础上扩展而来,专为鸿蒙系统应用开发而设计。ArkTS 在鸿蒙开发中占据着举足轻重的地位,它为开发者提供了一种高效、简洁的开发方式,能够帮助开发者快速构建出高性能、高质量的鸿蒙应用。
ArkTS 具有以下几个特点:
- 静态类型:ArkTS 继承了 TypeScript 的静态类型特性,在编译时进行严格的类型检查,可以提前发现类型错误,提高代码的稳定性和可维护性。例如:
typescript
let num: number = 10;
num = "hello"; // 编译时会报错,因为类型不匹配
- 声明式 UI:ArkTS 采用声明式的 UI 开发范式,开发者只需描述 UI 的最终状态,而无需关注 UI 的构建过程,大大提高了开发效率。例如,使用 ArkTS 创建一个简单的按钮:
typescript
@Entry
@Component
struct ButtonExample {
build() {
Column() {
Button("Click Me")
.onClick(() => {
console.log("Button clicked");
})
}
}
}
- 响应式编程:ArkTS 支持响应式编程,通过状态管理机制,当数据状态发生变化时,UI 会自动更新,实现数据与 UI 的双向绑定,简化了开发过程。例如:
typescript
@Entry
@Component
struct Counter {
@State count: number = 0;
build() {
Column() {
Text(`Count: ${this.count}`);
Button("Increment")
.onClick(() => {
this.count++;
});
}
}
}
- 跨平台:ArkTS 编写的代码可以在多种鸿蒙设备上运行,实现一次开发,多端部署,降低了开发成本,提高了开发效率 。这使得开发者能够为不同类型的设备(如手机、平板、智能手表、智能音箱等)提供统一的应用体验,无需为每个设备单独开发一套代码。例如,一个基于 ArkTS 开发的新闻阅读应用,可以在手机上以简洁的列表形式展示新闻内容,在平板上以分栏布局展示新闻列表和详细内容,在智能手表上则以简洁的通知形式提醒用户有新的新闻。
二、ArkTS 基础语法详解
2.1 变量与数据类型
在 ArkTS 中,变量用于存储数据,数据类型则决定了变量能够存储的数据种类和可以执行的操作。
变量声明使用let和const关键字。let声明的变量可以重新赋值,而const声明的常量一旦赋值就不能再改变。例如:
typescript
let count: number = 0;
count = 1; // 合法,count可以重新赋值
const PI: number = 3.14;
// PI = 3.14159; // 非法,PI是常量,不能重新赋值
ArkTS 支持多种基本数据类型:
-
number :表示数字,包括整数和浮点数,例如
10、3.14。 -
string :表示字符串,用于存储文本数据,例如
"Hello, ArkTS"。 -
boolean :表示布尔值,只有
true和false两个值,常用于条件判断 。 -
null:表示空值,代表一个空对象指针。
-
undefined :表示未定义,当一个变量声明但未初始化时,其值为
undefined。
除了基本数据类型,ArkTS 还支持一些复杂数据类型:
- 数组(Array):用于存储一组相同类型的数据,可以通过索引访问数组元素。例如:
typescript
let numbers: number[] = [1, 2, 3, 4, 5];
console.log(numbers[0]); // 输出 1
- 对象(Object):由一组键值对组成,用于存储和管理相关的数据。例如:
typescript
let person = {
name: "John",
age: 30,
profession: "Engineer"
};
console.log(person.name); // 输出 John
- 枚举(Enum):用于定义一组命名的常量。例如:
typescript
enum Color {
Red,
Green,
Blue
}
let myColor: Color = Color.Green;
console.log(myColor); // 输出 1,枚举值默认从0开始编号
2.2 函数定义与调用
函数是一段可重复使用的代码块,用于执行特定的任务。在 ArkTS 中,函数定义包括函数名、参数列表、返回值类型和函数体。
函数声明的基本语法如下:
typescript
function functionName(parameter1: type1, parameter2: type2): returnType {
// 函数体
return result;
}
例如,定义一个加法函数:
typescript
function add(a: number, b: number): number {
return a + b;
}
调用函数时,使用函数名并传入相应的参数:
typescript
let sum: number = add(3, 5);
console.log(sum); // 输出 8
在函数定义中,可以使用箭头函数来简化函数的写法,箭头函数通常用于定义简短的匿名函数。例如,上述加法函数可以用箭头函数表示为:
typescript
const add = (a: number, b: number): number => a + b;
ArkTS 还支持函数的可选参数和默认参数。可选参数在参数名后加?,表示该参数可以不传;默认参数为参数提供一个默认值,当调用函数时未传入该参数时,将使用默认值。例如:
typescript
function greet(name?: string, message: string = "Hello"): void {
if (name) {
console.log(`${message}, ${name}`);
} else {
console.log(message);
}
}
greet(); // 输出 Hello
greet("John"); // 输出 Hello, John
greet("John", "Hi"); // 输出 Hi, John
2.3 控制流语句
控制流语句用于控制程序的执行流程,根据不同的条件执行不同的代码块。
条件语句(if - else):根据条件的真假来决定执行哪个代码块。例如:
typescript
let score: number = 85;
if (score >= 90) {
console.log("优秀");
} else if (score >= 60) {
console.log("及格");
} else {
console.log("不及格");
}
循环语句(for、while) :用于重复执行一段代码。for循环通常用于已知循环次数的情况,while循环则用于在条件为真时持续循环。例如:
typescript
// for循环
for (let i: number = 0; i < 5; i++) {
console.log(i);
}
// while循环
let count: number = 0;
while (count < 5) {
console.log(count);
count++;
}
分支语句(switch - case):根据一个表达式的值来选择执行不同的代码块,适用于多条件判断的场景。例如:
typescript
let day: number = 3;
switch (day) {
case 1:
console.log("Monday");
break;
case 2:
console.log("Tuesday");
break;
case 3:
console.log("Wednesday");
break;
default:
console.log("Other day");
}
2.4 类与面向对象编程
类是面向对象编程的核心概念,它是一种抽象的数据类型,用于创建对象。在 ArkTS 中,类定义包括属性、方法和构造函数。
定义一个简单的类:
typescript
class Person {
// 属性
name: string;
age: number;
// 构造函数,用于初始化对象
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 方法
greet(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
创建类的实例并调用方法:
typescript
let person: Person = new Person("Alice", 25);
person.greet(); // 输出 Hello, my name is Alice and I am 25 years old.
类的属性可以分为私有属性和公有属性。私有属性在属性名前加private关键字,只能在类内部访问;公有属性可以在类外部访问。例如:
typescript
class Rectangle {
private width: number;
private height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
let rect: Rectangle = new Rectangle(5, 3);
// console.log(rect.width); // 非法,width是私有属性,不能在类外部访问
console.log(rect.getArea()); // 输出 15
类还支持继承,通过继承可以创建一个新类,新类继承了父类的属性和方法,并且可以添加自己的属性和方法。例如:
typescript
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): void {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
speak(): void {
console.log(`${this.name} barks.`);
}
}
let dog: Dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // 输出 Buddy barks.
在上述例子中,Dog类继承自Animal类,并重写了speak方法,体现了多态性。多态性使得不同的对象可以对相同的方法调用做出不同的响应 。
三、基于 ArkTS 的 UI 开发
3.1 声明式 UI 概念与优势
在传统的命令式 UI 开发中,开发者需要详细地描述如何构建和更新 UI,包括创建 UI 元素、设置属性、处理事件等一系列具体的操作步骤 。以创建一个简单的按钮为例,在命令式 UI 中,可能需要以下步骤:
typescript
// 创建按钮对象
let button = document.createElement('button');
// 设置按钮文本
button.textContent = 'Click Me';
// 添加点击事件处理函数
button.addEventListener('click', function() {
console.log('Button clicked');
});
// 将按钮添加到页面中
document.body.appendChild(button);
这种方式虽然灵活,但代码较为繁琐,尤其是在处理复杂 UI 时,开发者需要花费大量精力来管理 UI 的状态和更新过程。
而声明式 UI 则采用了一种不同的思路,开发者只需描述 UI 的最终状态,即 "是什么",而无需关心 UI 的构建过程。框架会根据开发者的描述自动生成和更新 UI。在 ArkTS 中,使用声明式 UI 创建相同按钮的代码如下:
typescript
@Entry
@Component
struct ButtonExample {
build() {
Column() {
Button("Click Me")
.onClick(() => {
console.log("Button clicked");
})
}
}
}
声明式 UI 具有以下优势:
-
代码简洁:无需编写大量的底层操作代码,减少了代码量,提高了开发效率。例如,在构建一个复杂的表单时,声明式 UI 可以通过简洁的代码描述表单的结构和样式,而命令式 UI 则需要逐个创建表单元素并设置其属性。
-
可读性好:代码结构与 UI 的层次结构更加相似,易于理解和维护。其他开发者可以快速了解 UI 的布局和功能。例如,在一个包含多个组件的页面中,声明式 UI 的代码可以清晰地展示各个组件的嵌套关系和属性设置。
-
维护方便:当 UI 需求发生变化时,只需修改声明式代码中的描述部分,框架会自动更新 UI,而无需手动修改大量的操作代码。例如,如果需要修改按钮的文本和样式,在声明式 UI 中,只需在相应的组件中修改属性值即可。
-
开发效率高:声明式 UI 通常结合响应式编程,能够自动处理数据变化对 UI 的影响,减少了手动更新 UI 的工作量。例如,当数据模型中的某个值发生变化时,与之绑定的 UI 元素会自动更新显示。
3.2 常用 UI 组件
Text 组件:用于显示文本内容,支持设置文本的字体、大小、颜色、对齐方式等属性。例如:
typescript
Text("Hello, ArkTS")
.fontSize(20)
.fontColor(Color.Blue)
.textAlign(TextAlign.Center)
Button 组件:用于创建按钮,支持设置按钮的文本、样式、点击事件等。按钮类型包括胶囊按钮、圆形按钮、普通按钮。Button 当做为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮。例如:
typescript
// 创建不包含子组件的普通按钮
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
.onClick(() => {
console.log('Button clicked');
})
// 创建包含子组件的按钮
Button({ type: ButtonType.Normal, stateEffect: true }) {
Row() {
Image($r('app.media.loading')).width(20).height(40).margin({ left:12 })
Text('loading').fontSize(12).fontColor(0xffffff).margin({ left:5, right: 12 })
}.alignItems(VerticalAlign.Center)
}.borderRadius(8).backgroundColor(0x317aff).width(90).height(40)
Image 组件:用于显示图片,支持设置图片的源路径、大小、缩放模式等属性。例如:
typescript
Image($r('app.media.icon'))
.width(100)
.height(100)
.objectFit(ImageFit.Cover)
TextInput 组件:用于接收用户的文本输入,支持设置输入框的提示文本、输入类型(如数字、密码等)、输入事件等。例如:
typescript
TextInput({
placeholder: '请输入用户名',
type: InputType.Text
})
.width('100%')
.height(40)
.onChange((value) => {
console.log('输入内容:', value);
})
3.3 布局容器的使用
Column 组件 :沿垂直方向布局的容器,其子元素按照垂直方向依次排列。可以通过space属性设置子元素垂直方向间距,通过alignItems属性设置子元素在水平方向上的对齐格式,通过justifyContent属性设置子元素在垂直方向上的对齐格式 。例如:
typescript
Column({ space: 10 })
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
{
Text('Item 1')
Text('Item 2')
Text('Item 3')
}
Row 组件 :沿水平方向布局的容器,其子元素按照水平方向依次排列。同样可以设置space、alignItems(此时设置子元素在垂直方向上的对齐格式)和justifyContent(设置子元素在水平方向上的对齐格式)等属性。例如:
typescript
Row({ space: 15 })
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)
{
Text('Left')
Text('Center')
Text('Right')
}
Stack 组件 :层叠布局容器,其子元素会按照添加的顺序进行层叠,后一个子元素会覆盖在前一个子元素之上。可以通过alignContent参数实现子元素位置的相对移动,支持九种对齐方式 。通过zIndex属性改变兄弟组件显示层级关系,zIndex值越大,显示层级越高。例如:
typescript
Stack({ alignContent: Alignment.TopStart })
{
Text('Bottom')
Text('Middle').zIndex(1)
Text('Top').zIndex(2)
}
List 组件 :用于展示列表数据,支持设置列表的方向(垂直或水平)、列表项之间的间距等属性。通过ForEach循环遍历数据数组来生成列表项。例如:
typescript
let data = ['Apple', 'Banana', 'Cherry'];
List({ space: 8, listDirection: Axis.Vertical })
{
ForEach(data, (item) => {
ListItem() {
Text(item)
}
}, item => item)
}
四、状态管理在鸿蒙应用中的实践
4.1 状态管理概念
在鸿蒙应用开发中,状态管理是构建响应式界面的核心支柱,而单向数据流(Unidirectional Data Flow, UDF)作为鸿蒙架构的重要设计原则,贯穿于组件通信、状态更新和界面渲染的全流程。
状态管理,简单来说,就是对应用程序中数据状态的管理和控制,以确保应用在不同场景下的正确行为和良好用户体验。它主要负责维护应用的数据模型,并确保数据的变化能够及时、准确地反映在用户界面(UI)上,实现数据与 UI 展示之间的同步。
以一个简单的计数器应用为例,计数器的当前数值就是一个状态。当用户点击 "增加" 或 "减少" 按钮时,这个状态会发生变化,而 UI 界面上显示的计数器数值也应该随之更新 。在这个过程中,状态的变化驱动了 UI 的更新,这就是状态驱动 UI 更新的概念。
单向数据流则是指数据在应用中按照固定方向流动:状态(State)驱动界面渲染,用户交互(Event)触发状态更新,形成「状态→视图→交互→新状态」的闭环。在鸿蒙中,这一过程通过 @State、@Prop 、@Link 等装饰器实现精准的状态绑定与更新,确保数据变化可追踪、可预测。其具有以下核心特征:
-
单一数据源:每个组件状态由唯一源头管理(如组件内 @State、父组件传递的 @Prop),避免数据不一致。
-
单向传递:状态从父组件到子组件单向流动,子组件通过回调通知父组件更新,杜绝循环依赖。
-
声明式更新:只需修改状态值,框架自动完成界面重渲染,无需手动操作 DOM。
状态容器则是存储和管理状态的机制,可以将状态管理比喻为 "数据盒子":当 "盒子" 里的数据变化时,使用这些数据的 UI 会自动更新,且只能通过特定方式修改 "盒子" 里的数据,确保可预测性。
4.2 常用状态装饰器
@State 装饰器:用于声明组件的内部状态变量。被 @State 修饰的变量,当它的值发生变化时,会自动触发组件的重新渲染,从而实现 UI 的更新。它是最基本的状态管理装饰器,主要用于管理组件自身的状态。例如在一个计数器组件中:
typescript
@Entry
@Component
struct Counter {
@State count: number = 0;
build() {
Column() {
Text(`${this.count}`)
.fontSize(30)
.fontWeight(FontWeight.Bold);
Button("增加")
.onClick(() => {
this.count++;
});
Button("减少")
.onClick(() => {
this.count--;
});
}
}
}
在上述代码中,count 变量被 @State 修饰,当用户点击 "增加" 或 "减少" 按钮,count 的值发生变化时,Text 组件会自动更新显示最新的 count 值。
@Link 装饰器:用于建立父子组件之间的双向数据绑定,实现父子组件之间的数据双向同步。例如,在一个包含输入框和显示文本的父子组件结构中:
typescript
// 子组件
@Component
struct TextInput {
@Link textValue: string;
label: string = "输入内容";
build() {
Column() {
Text(this.label);
TextInput({ text: this.textValue })
.onChange((value: string) => {
this.textValue = value;
});
}
}
}
// 父组件
@Component
struct HomePage {
@State message: string = "你好";
build() {
Column({ space: 20 }) {
Text(`父组件显示的消息: ${this.message}`);
TextInput({ textValue: $message, label: '编辑消息' });
}
}
}
在上述代码中,父组件 HomePage 的 message 状态变量通过 @Link 与子组件 TextInput 的 textValue 建立双向绑定。在 TextInput 子组件中输入内容,textValue 变化会同步到父组件的 message 上;父组件中 message 变化也会同步到子组件的 textValue 上 。
4.3 状态管理最佳实践
最小状态原则:只将需要驱动 UI 的数据声明为状态变量,避免不必要的状态声明,减少状态管理的复杂性。例如,在一个展示用户信息的组件中,如果用户的头像图片地址不会在组件内发生变化,就不需要将其声明为状态变量。
typescript
// 正确做法,只将需要响应式更新的用户名声明为状态
@Entry
@Component
struct UserProfile {
@State username: string = "John";
build() {
Column() {
Image($r('app.media.avatar'))
.width(100)
.height(100);
Text(this.username);
}
}
}
状态提升 :当多个组件需要共享状态时,将状态提升到共同的父组件。这样可以确保状态的唯一性和一致性,避免数据不一致的问题。例如,有两个子组件 Child1 和 Child2 都需要使用某个状态 sharedState:
typescript
// 父组件
@Entry
@Component
struct Parent {
@State sharedState: string = "初始值";
build() {
Column() {
Child1({ state: this.sharedState });
Child2({ state: this.sharedState });
}
}
}
// 子组件1
@Component
struct Child1 {
@Prop state: string;
build() {
Text(this.state);
}
}
// 子组件2
@Component
struct Child2 {
@Prop state: string;
build() {
Text(this.state);
}
}
不可变性 :尽量避免直接修改复杂对象,而是创建新对象。直接修改复杂对象可能会导致状态变化无法被正确检测,从而影响 UI 的更新。例如,对于一个包含用户信息的对象 user:
typescript
// 错误做法,直接修改对象属性,可能无法触发UI更新
@Entry
@Component
struct UserComponent {
@State user = { name: "John", age: 30 };
updateUser() {
this.user.age = 31;
}
build() {
Column() {
Text(`Name: ${this.user.name}, Age: ${this.user.age}`);
Button("Update Age")
.onClick(() => {
this.updateUser();
});
}
}
}
// 正确做法,创建新对象,确保状态变化被检测到
@Entry
@Component
struct UserComponent {
@State user = { name: "John", age: 30 };
updateUser() {
this.user = { ...this.user, age: 31 };
}
build() {
Column() {
Text(`Name: ${this.user.name}, Age: ${this.user.age}`);
Button("Update Age")
.onClick(() => {
this.updateUser();
});
}
}
}
状态分层 :区分局部状态(@State)和全局状态(AppStorage)。局部状态只在组件内部使用,全局状态则可以在整个应用中共享。例如,用户的登录状态可以作为全局状态存储在 AppStorage 中,而某个组件内的临时数据则作为局部状态使用 @State 管理。
typescript
// 使用AppStorage管理全局状态
import AppStorage from '@ohos.appstorage';
// 存储全局状态
AppStorage.SetOrCreate('isLoggedIn', true);
// 获取全局状态
let isLoggedIn = AppStorage.Get('isLoggedIn');
避免过度状态:不要将所有数据都声明为状态,只关注影响 UI 的数据。过度的状态声明会增加应用的内存消耗和状态管理的难度。例如,一些只在组件内部计算使用,且不影响 UI 显示的数据,就不需要声明为状态变量。
五、鸿蒙应用开发实战:构建简单应用
5.1 开发环境搭建
要进行鸿蒙应用开发,首先需要搭建开发环境,主要工具是 DevEco Studio。以下是搭建步骤:
-
下载 DevEco Studio :访问华为开发者联盟官网(https://developer.huawei.com/consumer/cn/),在下载中心找到 DevEco Studio,根据你的操作系统(Windows 或 macOS)选择对应的安装包进行下载。
-
安装 DevEco Studio:下载完成后,双击安装包进行安装。以 Windows 系统为例,在安装向导中,选择安装路径(建议安装在非系统盘,如 D 盘),勾选 "创建桌面快捷方式" 和 "添加到环境变量" 选项,然后按照提示完成安装过程。安装过程中会自动下载 OpenJDK、Gradle 等工具 。
-
配置 Node.js 环境 :DevEco Studio 依赖 Node.js 环境,若本地未安装 Node.js,首次启动 DevEco Studio 时,IDE 会提示自动安装(安装路径不能含空格)。也可以自行前往 Node.js 官网(https://nodejs.org/en/download/)下载并安装最新版本的 Node.js。安装完成后,在命令行中输入
node -v,检查 Node.js 是否安装成功。 -
配置 SDK:打开 DevEco Studio,进入 "Settings"(Windows 系统下为 "File"->"Settings"),在搜索框中输入 "HarmonyOS SDK",选择 "HarmonyOS SDK" 选项,设置 SDK 的存储路径(如 D:\Huawei\SDK),勾选 "Accept" 协议,等待 SDK 自动下载完成。此过程可能需要一些时间,请耐心等待。
-
安装模拟器:在 DevEco Studio 的欢迎界面,点击 "Configure"->"AVD Manager",在弹出的窗口中点击 "Create Device",选择合适的模拟器设备(如 Phone、Tablet 等),然后点击 "Next",选择系统镜像(如 API 12 及以上版本),最后点击 "Finish" 完成模拟器的创建。创建完成后,在 AVD Manager 中点击模拟器的 "Play" 按钮启动模拟器。
5.2 项目结构解析
以一个简单的鸿蒙应用项目为例,其目录结构如下:
Plain
TodoApp/
├── AppScope/ # 应用全局配置
│ └── resources/ # 应用全局资源
│ └── base
│ ├── element
│ │ └── string.json
│ └── media
│ └── app_icon.png
│ └── app.json5 # 应用全局配置文件
├── entry/ # 主应用模块
│ ├── src/main/ets/ # 源代码目录
│ │ ├── entryability/ # 应用入口
│ │ │ └── EntryAbility.ts # 应用生命周期管理
│ │ ├── pages/ # 页面目录
│ │ │ ├── Index.ets # 应用首页
│ │ │ ├── AddTodo.ets # 添加待办页面
│ │ │ └── TodoDetail.ets# 待办详情页面
│ │ ├── components/ # 自定义组件目录
│ │ ├── models/ # 数据模型目录
│ │ └── services/ # 业务服务目录
│ ├── src/main/resources/ # 模块资源目录
│ │ ├── base
│ │ │ ├── element
│ │ │ ├── media
│ │ │ └── profile
│ │ └── rawfile
│ └── module.json5 # 模块配置文件
├── build-profile.json5 # 项目构建配置
└── oh_modules # 用于存放三方库依赖信息
-
AppScope 目录:应用的全局配置和资源文件夹,通常包含一些全局资源和配置,供应用的各个模块共享。
-
resources:用于存放应用的资源文件,可以细分为多个子目录。
-
base:存放应用的基础资源,包括语言资源、样式资源等。
-
element:定义了应用中的基础元素,如字符串、数字、颜色、样式等资源。常见的如 string.json,其中存放了应用中会用到的字符串文本内容。
-
media:存放应用中的多媒体资源,如图标、背景图、音频等。
-
-
app.json5:此文件包含了应用的全局配置信息,通常包括应用名称、版本号、设备配置、权限声明等。
-
-
entry 目录:开发的核心部分,包含了应用模块的源代码、资源、配置以及构建任务。
-
src:存放应用的源代码,主要是 ArkTS 的代码部分,分为以下几个子目录。
-
ets:存放 ArkTS 源码,包括应用的入口、扩展能力以及页面定义。
-
entryability:定义应用的入口能力,通常会包含应用启动后的初始化逻辑。
-
pages:应用中的各个页面模块,通常是应用的各个视图层或 UI 层。每个页面对应一个.ets 文件,包含该页面的 UI 布局、业务逻辑、事件处理等内容。
-
components:存放可复用的自定义组件。
-
models:存放数据模型定义。
-
services:存放业务逻辑和服务。
-
-
resources:存放应用使用到的资源文件,类似 AppScope/resources,但一般这是局部资源,供当前模块使用。
-
base、media、profile:资源文件的具体分类。base 可能包括一些通用的样式文件,media 包括图片、音频等,profile 则是配置信息。
-
rawfile:原始资源文件,通常是应用中不需要设备适配的静态文件,路径和名称需要明确指定。
-
-
-
module.json5:模块的配置文件,包含了当前模块的基本配置信息,包括模块名称、版本、依赖关系等,HAP 包的配置信息,描述了应用如何在目标设备上运行,以及不同设备之间的适配,应用 / 服务的全局配置信息。
-
-
build-profile.json5:该文件存储模块级的编译配置项,包括编译选项、目标设备或平台等。
-
oh_modules 目录:用于存放应用的第三方库依赖,类似于 Node.js 中的 node_modules。
5.3 资源管理基础
在鸿蒙应用开发中,资源管理是一个重要的环节,它可以帮助我们更好地组织和管理应用中的各种资源,如颜色、字符串、尺寸、媒体资源等。
-
资源分类:
- 颜色资源 :用于定义应用中的各种颜色,通常存储在
resources/base/element/color.json文件中。例如:
- 颜色资源 :用于定义应用中的各种颜色,通常存储在
json
{
"primary_color": "#007DFF",
"secondary_color": "#F5F5F5"
}
在代码中引用颜色资源:
typescript
Text("Hello")
.fontColor($r('app.color.primary_color'))
- 字符串资源 :用于存储应用中的文本内容,方便进行国际化和本地化。字符串资源存储在
resources/base/element/string.json文件中。例如:
json
{
"app_name": "My Todo App",
"welcome_message": "Welcome to your todo list"
}
在代码中引用字符串资源:
typescript
Text($r('app.string.app_name'))
- 尺寸资源 :用于定义应用中的各种尺寸,如字体大小、边距、间距等,存储在
resources/base/element/float.json文件中。例如:
json
{
"font_size_large": 20,
"margin_small": 5
}
在代码中引用尺寸资源:
typescript
Text("Hello")
.fontSize($r('app.float.font_size_large'))
.margin({ top: $r('app.float.margin_small') })
- 媒体资源 :包括图片、音频、视频等文件,通常存储在
resources/base/media目录下。例如,将一张图片icon.png放在该目录下,在代码中引用图片资源:
typescript
Image($r('app.media.icon'))
.width(50)
.height(50)
-
资源目录结构 :应用的资源文件统一存放于
resources目录下,包括base目录与限定词目录。-
base 目录:默认存在,当没有匹配的限定词目录时自动引用,其中的资源文件会被编译成二进制文件,并赋予资源文件 ID,通过指定资源类型(type)和资源名称(name)引用。
-
限定词目录 :可以由一个或多个表征应用场景或设备特征的限定词组合而成,包括移动国家码和移动网络码、语言、文字、国家或地区、横竖屏、设备类型、颜色模式和屏幕密度等维度,限定词之间通过下划线(_)或者中划线(-)连接。例如,
en_US表示美式英语的限定词目录,zh_CN-car-ldpi表示适用于中国地区、简体中文、车机设备且屏幕密度为 ldpi 的限定词目录 。限定词目录中的资源文件同样会被编译成二进制文件,并赋予资源文件 ID,通过指定资源类型(type)和资源名称(name)来引用。 -
rawfile 目录 :支持创建多层子目录,子目录名称可以自定义,文件夹内可以自由放置各类资源文件。目录中的资源文件会被直接打包进应用,不经过编译,也不会被赋予资源文件 ID,通过指定文件路径和文件名引用。例如,访问
rawfile目录下的fonts/MyFont.ttf文件:
-
typescript
let fontPath = $rawfile('fonts/MyFont.ttf');
5.4 应用开发步骤与代码实现
下面以构建一个简单的待办事项应用为例,讲解鸿蒙应用的开发步骤与代码实现:
-
创建项目:打开 DevEco Studio,点击 "Create Project",选择 "Empty Ability" 模板,点击 "Next"。在项目配置界面,填写项目名称(如 TodoApp)、包名(如 com.example.todo)、保存路径,选择编译 SDK 版本和设备类型(如 Phone),语言选择 ArkTS,然后点击 "Finish",等待项目创建完成。
-
设计界面:
- 首页(Index.ets):展示待办事项列表,包含页面标题、添加按钮和待办事项列表。当没有待办事项时,显示空状态提示。
typescript
@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);
}
});
};
}, item => item.id);
}
.padding(10)
.flexGrow(1);
}
}
.width("100%")
.height("100%")
.backgroundColor("#F5F5F5");
}
// 页面加载时获取数据
aboutToAppear() {
this.loadTodos();
}
// 从本地存储加载待办事项
async loadTodos() {
// 模拟加载延迟
setTimeout(async () => {
this.isLoading = false;
// 实际应用中从本地存储读取数据
// this.todos = await StorageService.getTodos();
}, 1000);
}
// 更新待办事项状态
async updateTodoStatus(id: string, isCompleted: boolean) {
// 实际应用中更新本地存储
// await StorageService.updateTodoStatus(id, isCompleted);
this.todos = this.todos.map(todo => {
if (todo.id === id) {
todo.completed = isCompleted;
}
return todo;
});
}
// 删除待办事项
async deleteTodo(id: string) {
// 实际应用中从本地存储删除数据
// await StorageService.deleteTodo(id);
this.todos = this.todos.filter(todo => todo.id!== id);
}
}
- 添加页(AddTodo.ets):提供一个表单,用于添加新的待办事项,包含标题输入框、内容输入框和提交按钮。
typescript
@Entry
@Component
struct AddTodoPage {
@State title: string = "";
@State content: string = "";
build() {
Column() {
TextInput({
placeholder: "请输入标题",
text: this.title
})
.width("100%")
.height(40)
.margin({ top: 20 });
TextInput({
placeholder: "请输入内容",
text: this.content,
type: InputType.Multiline
})
.width("100%")
.height(150)
.margin({ top: 20 });
Button("提交")
.width("100%")
.height(40)
.backgroundColor("#007AFF")
.fontColor(Color.White)
.margin({ top: 20 })
.onClick(() => {
if (this.title && this.content) {
let newTodo: TodoItem = {
id: Date.now().toString(),
title: this.title,
content: this.content,
completed: false,
createTime: Date.now()
};
// 实际应用中保存到本地存储
// await StorageService.addTodo(newTodo);
// 刷新首页数据
router.back();
}
});
}
.width("100%")
.height("100%")
.padding(20);
}
}
-
编写逻辑:
- 数据模型(TodoModel.ets):定义待办事项的数据结构。
typescript
export interface TodoItem {
id: string;
title: string;
content: string;
completed: boolean;
createTime: number;
}
- 存储服务(StorageService.ets) :负责与本地存储进行交互,实现待办事项的增、删、改、查功能(这里仅为示例,实际实现需要使用
@ohos.data.preferences等相关模块)。
typescript
import preferences from '@ohos.data.preferences';
export class StorageService {
private static instance: StorageService;
private pref: preferences.Preferences | null = null;
private readonly STORAGE_KEY = 'todo_items';
private readonly PREF_NAME = 'todo_storage';
private constructor() {
this.init();
}
static getInstance(): StorageService {
if (!StorageService.instance) {
StorageService.instance = new StorageService();
}
return StorageService.instance;
}
async init() {
try {
this.pref = await preferences.getPreferences(this.PREF_NAME);
} catch (error) {
console.error('Failed to init storage:', error);
}
}
async addTodo(todo: TodoItem) {
if (!this.pref) return;
let todos = await this.getTodos();
todos.push(todo);
await this.pref.putJson(this.STORAGE_KEY, todos);
await this.pref.flush();
}
async getTodos(): Promise<TodoItem[]> {
if (!this.pref) return [];
let todos = await this.pref.getJson(this.STORAGE_KEY, []);
return todos.map((todo: any) => ({
...todo,
createTime: typeof todo.create