鸿蒙(HarmonyOS)是由华为推出的操作系统,面向手机、平板、智能穿戴、智慧屏等多种设备。它的特点是:

支持多设备形态

统一开发框架

声明式 UI 编程

兼容主流应用开发习惯

只需要会基本的编程操作,一步一步完成环境安装、项目创建与运行。便可在最短时间内完成一个可运行的鸿蒙应用。


一、环境准备

鸿蒙应用开发使用官方 IDE:DevEco Studio。它类似于 Android Studio 或 Xcode。

打开官网:

https://developer.huawei.com/consumer/cn/deveco-studio/

进入下载页面,选择最新版 DevEco Studio。

下载完成后直接安装。

打开DevEco,新建项目,可以选择 APP 还是元服务,元服务不需要下载安装,点击即用

选择 application,默认配置,点击finish创建项目

项目创建完成后,会看到下面的目录:

entry/

 ├── src/main/

 │    ├── ets/

 │    │    ├── entryability/

 │    │    └── pages/

 │    │         └── Index.ets

entry是主模块根目录;entryability是应用入口 Ability 的代码;pages/Index.ets为默认首页文件

右上角选择设备为previewer预览器,并点击运行

可以看到在手机上的运行效果

点击device-设备管理器,我们可以下载多种鸿蒙设备的模拟器,下载一个手机镜像

下载完成后,以mate80为例,新建设备,点击绿色三角运行

弹出模拟器运行窗口,手机旁菜单栏中可进行摇一摇、截屏等一系列手机操作

在右上角设备中选择新创建的mate80,运行。在模拟器窗口中看到运行效果


二、Arkts 基础

ArkTS在 TS 的基础上演化而来,而TypeScript (TS)源于JavaScript (JS)。ArkTS的特点是引入了声明式 UI 描述和状态管理机制;

2.1  变量与数据类型

在 ArkTS 中对变量的要求比较严格,必须明确告诉程序变量是什么类型,禁止使用 any 类型

常见的变量类型有string (字符串),必须用引号包起来;number (数字):装整数或小数;boolean (布尔值);Array (数组)等

定义变量时需要明确数据类型,同时使用let或const等关键字定义变量的属性

let title: string = '数据类型'; // let 定义可以修改的变量
const maxCount: number = 10;   // const 定义不能修改的常量
let isDone: boolean = false;

2.2  interface

interface定义了一个对象(Object)应该长什么样

例如描述一个待办任务可能要写三个独立的变量:

let todoId: number = 1;
let todoTitle: string = '去买菜';
let todoIsDone: boolean = false;

interface中把这三个属性打包,定义一个名为 Todo 的接口

interface Todo {
  id: number;      // 每一个 Todo 必须有一个数字类型的 id
  title: string;   // 每一个 Todo 必须有一个字符串类型的标题
  done: boolean;   // 每一个 Todo 必须有一个布尔类型的完成状态
}

定义好接口后,我们就可以根据它来创建真正的对象了。

// 声明一个变量 newTask,它的类型必须符合 Todo 接口
let newTask: Todo = {
  id: 1,
  title: '安装 DevEco Studio',
  done: true
};

Arkts中,接口也是非常严格的,如果在 newTask 里漏掉了某个属性,或添加了不存在的属性,编译器都会报错,且数据类型严格一致

若要添加可选属性,用 ? 标识

memo?: string; // 备注是“可选”的,不填也不会报错。

只读属性用 readonly 标识,一旦确定就不允许再修改

readonly id: number;

2.3  函数与方法

在 ArkTS 中,函数是执行特定任务的代码块。方法是定义在类(Class)或结构体(Struct)内部的函数。在鸿蒙开发中,写在组件里的逻辑代码(toggleDone)通常被称为方法

// 这是一个计算两个数字之和的函数
function add(a: number, b: number): number {
  let sum = a + b;
  return sum; // 返回计算结果
}

与很多语言相同,函数包含输入输出与函数体。括号中的(a: number, b: number)是函数的输入参数,必须标注类型;: number是函数的返回类型,如果函数不返回任何东西,类型写 : void;被花括号 { }围着的是函数体

ArkTS 的 UI 中有一种长得很像“箭头”的简写方式,是箭头函数。它非常适合作为按钮点击等事件的回调。

// (参数) => { 逻辑 }
.onClick(() => {
  console.log('点击了按钮');
})

2.4  声明式 UI 布局

在原生 Android或老iOS里,代码的是界面怎么一步步变化,状态变了,需要自己找到某个按钮,更改它的属性/结构。而在声明式UI 中,只需要描述最终状态,系统会自动把对应的组件挪到正确的位置。

每一个页面或组件都遵循固定的模板

@Entry       // 页面的入口,从这里开始构建 UI
@Component   // 声明这是一个 UI 组件
struct Index { // struct:结构体,定义组件
  
  // 数据/状态,比如 @State count: number = 0;

  build() {    //  UI 描述
    // 所有的 UI 代码必须写在这里
  }
} 

@Component表示下面的 struct 不是普通的数据结构,而是一个可参与 UI 构建、可被渲染、可被重组的组件

组件里通常分两块:

状态/数据区用来声明数据来源和数据流关系。如@State、@Prop、@Link、@ObjectLink 之类的装饰器字段。@State是组件内部自有状态。更改它会触发该组件及相关子树的重新构建,从而刷新界面。@Prop是父组件传进来的只读参数;@Link和父组件做双向绑定……负责更新保存数据

build() 区域是UI 描述区,写法是嵌套组件树,每当状态变化时,build() 会被重新执行,把变化反映到屏幕上。

要把组件整齐地摆放,需要容器。最常用的是Column和Row

Column让里面的内容从上到下垂直排列。Row从左到右水平排列

Column({ space: 20 }) { // 容器内组件间距为 20vp
  Text('我的待办清单')
    .fontSize(24)
  
  Row() {
    Text('任务 1:学习 ArkTS')
    Blank() // 空白组件,把后面的组件推到最右边
    Toggle({ type: ToggleType.Checkbox }) // 一个开关
  }
  .width('100%') 
}

改变组件的外观只需要在组件后面像接龙一样,点(.)出需要的属性。被称为链式调用。如:

      Text('我的待办清单')
        .fontSize(24) // 字号
        .fontColor(Color.Black) // 颜色
        .fontWeight(700)        // 加粗
        .backgroundColor('#007DFF') // 背景色(蓝色)
        .padding(10)            // 内边距
        .borderRadius(8)        // 圆角

完成布局后,需要实现对用户操作的响应,在组件后面链式调用事件方法即可,通常配合箭头函数。如在页面中加一个按钮

Button('点我')
  .onClick(() => {
    console.info('用户点了我一下');
  })

若需要自定义的响应函数,通常在组件中编写方法,build中使用this调用组件内的方法,如

@Component
struct TodoList {
  // 定义一个删除方法,参数是任务的 ID
  private removeTodo(id: number): void {
    console.log('正在删除编号为' + id + '的任务');
    // 删除逻辑
  }

  build() {
    Button('删除任务')
      .onClick(() => {
        this.removeTodo(101); // 通过 this 调用组件内的方法
      })
  }
}

三、ToDo Lite 待办应用

接下来我们打造一个最简化功能的ToDo Lite 应用。

新建项目,打开entry/src/main/ets/pages/index.ets文件

首先定义 Todo这个接口,包含三个属性

interface Todo {
  id: number;     // id
  title: string;  // 待办的内容
  done: boolean;  // 是否完成
}

第二步写出基础布局

在 struct Index 中,给列表塞两个假数据,并在build中画出ui

在把 this.todos 这个数组,一条一条地渲染成列表项时,使用循环函数

ForEach(this.todos, (item: Todo) => { ... }, key)

ForEach 对数组 this.todos 里的每一个元素执行一遍回调函数,然后生成对应的 UI。

等价于伪代码:

for (let item of this.todos) {
    生成一个 ListItem
}

(item: Todo) => item.id.toString()是key参数,UI 框架需要知道,当数组发生变化时,哪一项是“同一个元素”,哪一项是新加的。item.id 是每个 Todo 的唯一身份。

List函数的作用是把 todos 数组里的每个 Todo 渲染成一条带标题和删除按钮的列表项,并用 id 作为唯一标识

      List() {
        ForEach(this.todos, (item: Todo) => {
          ListItem() {
            Row() {
              Text(item.title).fontSize(18).layoutWeight(1)
              Button('删除').type(ButtonType.Capsule)
            }.width('100%').padding(16)
          }
        }, (item: Todo) => item.id.toString()) // 唯一标识符
      }

完整代码为

interface Todo {
  id: number;     // id
  title: string;  // 待办的内容
  done: boolean;  // 是否完成
}

@Entry
@Component
struct Index {
  @State todos: Todo[] = [
    { id: 1, title: '安装 DevEco Studio', done: true },
    { id: 2, title: '新建一个项目', done: false }
  ];

  build() {
    Column({ space: 12 }) {
      // 标题行
      Row() {
        Text('ToDo Lite').fontSize(24).fontWeight(FontWeight.Bold)
        Blank()
        Button('新增').type(ButtonType.Capsule)
      }.width('100%').padding(16)

      // 列表
      List() {
        ForEach(this.todos, (item: Todo) => {
          ListItem() {
            Row() {
              Text(item.title).fontSize(18).layoutWeight(1)
              Button('删除').type(ButtonType.Capsule)
            }.width('100%').padding(16)
          }
        }, (item: Todo) => item.id.toString()+ item.done) // 唯一标识符
      }
    }
  }
}

这样只实现了界面,点哪里都没反应。我们要加入切换状态和删除的逻辑。

toggleDone 切换完成状态。生成一个新数组;找到那条 id 对应的 todo,复制一份并把 done 取反;最后整体替换 this.todos。

private toggleDone(id: number) {
  this.todos = this.todos.map(t => {
    if (t.id === id) {
      return { id: t.id, title: t.title, done: !t.done } as Todo;
    }
    return t;
  });
}

removeTodo删除任务

private removeTodo(id: number) {
  // 只保留那些 ID 不匹配的任务
  this.todos = this.todos.filter(t => t.id !== id);
}

两个方法定义完成后,将它们绑定到 UI 上。给任务行添加

.onClick(() => this.toggleDone(item.id))

给删除按钮添加

.onClick(() => this.removeTodo(item.id))

同时需要给切换状态加上待办完成效果

              Text(item.title)
                .fontSize(18)
                .decoration({
                  type: item.done ? TextDecorationType.LineThrough : TextDecorationType.None
                })
                .opacity(item.done ? 0.5 : 1.0)
                .layoutWeight(1)

.decoration是一种三元表达式。系统会检查 item.done 是 true 还是 false。如果是 true,就应用 LineThrough(删除线),同时将已完成字体变淡一点

.opacity(item.done ? 0.5 : 1.0)

此时,待办完成功能可以正常使用,接下来完成新建待办的功能

用户点击新建时,要去往“新增页面”,在 pages 文件夹下新建 AddTodo.ets

首先完成ui部分

@Entry
@Component
struct AddTodo {
  @State title: string = ''; // 输入的文字

  build() {
    Column({ space: 16 }) {
      Text('新增待办').fontSize(22).fontWeight(FontWeight.Bold)

      TextInput({ placeholder: '请输入待办事项...' })
        .onChange((value: string) => { this.title = value; }) // 实时同步文字

      Button('保存').onClick(() => {
        // 这里需要把数据传回去
      })
    }.padding(16)
  }
}

TextInput是一个输入框,placeholder 是占位提示:没输入时灰色显示“请输入待办事项...”

onChange(...)只要输入框里的内容变化就触发,将输入内容同步给title

在新页面中得到的数据需要同步在主页面中,AppStorage是一个全app可见的储藏空间

在 AddTodo.ets 中存入:

  private save() {
    const v = this.title.trim();
    if (v.length === 0) return;
    // 将新待办的标题存入全局存储
    AppStorage.setOrCreate('newTodoTitle', v);
    // 关闭当前 AddTodo 页面,回到上一个页面
    router.back();
  }

在 Index.ets 中取出:

利用 onPageShow ,每次页面一显示就去检查储藏空间。

  onPageShow() {
    const newTitle: string | undefined = AppStorage.get('newTodoTitle');
    // 如果有新标题且不为空,则添加
    if (newTitle && newTitle.trim().length > 0) {
      const newItem: Todo = {
        id: Date.now(),
        title: newTitle.trim(),
        done: false
      };
      // 更新数组
      this.todos = [newItem, ...this.todos];
      // 读取完后立即清理全局存储,防止逻辑重复执行
      AppStorage.setOrCreate('newTodoTitle', '');
    }
  }

给新建按钮添加回调函数,转到新页面

.onClick(() => { router.pushUrl({ url: 'pages/AddTodo' }) }) 

需要在文件顶部写

import router from '@ohos.router'

给新建页面的按钮添加回调函数

Row({ space: 12 }) {
        Button('取消').onClick(() => router.back())
        Button('保存').onClick(() => this.save())
      }

路由能跳转的页面必须在配置里登记,最后将AddTodo 页面加入 pages 配置

entry/src/main/resources/base/profile/main_pages.json

加入AddTodo

{
  "src": [
    "pages/Index",
    "pages/AddTodo"
  ]
}

全部完成之后点击运行

点击新建,输入新待办并保存,返回主页面,待办出现在列表最上方,功能正常

Index.ets完整内容

import router from '@ohos.router'

interface Todo {
  id: number;     // id
  title: string;  // 待办的内容
  done: boolean;  // 是否完成
}

@Entry
@Component
struct Index {
  @State todos: Todo[] = [
    { id: 1, title: '安装 DevEco Studio', done: true },
    { id: 2, title: '新建一个项目', done: false }
  ];
  onPageShow() {
    // 从全局存储读取传回来的标题
    const newTitle: string | undefined = AppStorage.get('newTodoTitle');
    // 如果有新标题且不为空,则添加
    if (newTitle && newTitle.trim().length > 0) {
      const newItem: Todo = {
        id: Date.now(),
        title: newTitle.trim(),
        done: false
      };
      // 更新数组
      this.todos = [newItem, ...this.todos];
      // 读取完后立即清理全局存储,防止逻辑重复执行
      AppStorage.setOrCreate('newTodoTitle', '');
    }
  }
  // 切换状态
  private toggleDone(id: number) {
    this.todos = this.todos.map(t => {
      if (t.id === id) {
        // 返回一个“新”对象,触发 UI 刷新
        return { id: t.id, title: t.title, done: !t.done } as Todo;
      }
      return t;
    });
  }

  // 删除任务
  private removeTodo(id: number) {
    this.todos = this.todos.filter(t => t.id !== id);
  }

  build() {
    Column({ space: 12 }) {
      // 标题行
      Row() {
        Text('ToDo Lite').fontSize(24).fontWeight(FontWeight.Bold)
        Blank()
        Button('新增')
          .type(ButtonType.Capsule)
          .onClick(() => {
            router.pushUrl({ url: 'pages/AddTodo' })
          })
      }.width('100%')
      .padding({ left: 16, right: 16, top: 16 })

      // 列表
      List() {
        ForEach(this.todos, (item: Todo) => {
          ListItem() {
            Row() {
              Text(item.title)
                .fontSize(18)
                .decoration({
                  type: item.done ? TextDecorationType.LineThrough : TextDecorationType.None
                })
                .opacity(item.done ? 0.5 : 1.0)
                .layoutWeight(1)


              Button('删除').type(ButtonType.Capsule).onClick(() => this.removeTodo(item.id))
            }.width('100%').padding(16).onClick(() => this.toggleDone(item.id))
          }
        }, (item: Todo) => item.id.toString()+ item.done) // 唯一标识符
      }
    }
  }
}

AddTodo.ets 完整内容

import router from '@ohos.router'


@Entry
@Component
struct AddTodo {
  @State title: string = ''; // 输入的文字
  private save() {
    if (this.title.trim().length === 0) return;
    // 存入"newTodoTitle"
    AppStorage.setOrCreate('newTodoTitle', this.title.trim());
    router.back(); // 回到上一页
  }


  build() {
    Column({ space: 16 }) {
      Text('新增待办').fontSize(22).fontWeight(FontWeight.Bold)

      TextInput({ placeholder: '请输入待办事项...' })
        .onChange((value: string) => { this.title = value; }) // 实时同步文字

      Row({ space: 12 }) {
        Button('取消').onClick(() => router.back())
        Button('保存').onClick(() => this.save())
      }
    }.padding(16)
  }
}

从环境搭建开始,到创建工程、编写页面、实现状态管理、完成页面跳转,最终实现了一个最简单的可用的 ToDo 应用。当然,上架应用商店的标准比这高得多,我们需要更有创新、更加丰富的应用

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐