鸿蒙-1. 组件基础

开发环境

1. 起步-鸿蒙简介

介绍:

  • HarmonyOS 是新一代的智能终端操作系统,为不同设备的智能化、互联与协同提供了统一的语言。带来简洁,流畅,连续,安全可靠的全场景交互体验。

历程:

时间 事件
2019 HarmonyOS 1.0,华为在东莞举行华为开发者大会,正式发布操作系统鸿蒙 OS,主要用于物联网
2020 HarmonyOS 2.0,基于开源项目 OpenHarmony 开发的面向多种全场景智能设备的商用版本
2021 HarmonyOS 3.0,先后优化游戏流畅度、地图三维体验、系统安全,另外系统的稳定性也得到了增强
2023.2 HarmonyOS 3.1,系统纯净能力进一步提升,对后台弹窗、 隐藏应用、后台跳转等情况
2023.7 华为 Mate 50 系列手机获推 HarmonyOS 4.0
2024 HarmonyOS Next 即将发布,将不在兼容安卓应用

和我们有什么关系?

  • 环境弱行业卷,怎么办?
  • 纯鸿蒙应用开发潮,你跟不跟?
  • 前端技术迁移鸿蒙开发易上手,你学不学?

2. 起步-DevEco Studio

安装 DevEco Studio 编辑器

  1. 下载:https://developer.harmonyos.com/cn/develop/deveco-studio#download
    • Windows(64-bit)
    • Mac(X86)
    • Mac(ARM)
  2. 安装:DevEco Studio → 一路 Next
  3. 运行:
    • 基础安装:Node.js >= 16.9.1 + Install ohpm 鸿蒙包管理器
    • SDK 安装
    • 安装完毕

3. 起步-HelloWorld

创建一个空项目:

  1. Create Project

  2. 选择项目模板

  3. 填写项目信息

  4. Finish

4. 起步-效果预览

效果预览方法:

  • Previewer 预览模式
  • Local Emulator 本地模拟器
  • Remote Emulator 远程模拟器
  • Remote Device 远程真机
  • Local Device 本地真机
推荐使用
  • Previewer 预览 和 Local Emulator 本地模拟器;
  • 尤其推荐使用 遥遥领先 真机调试;

1)Previewer 预览

场景:静态页面(没有组件间数据通信、不涉及到网络请求)
条件:有 @Entry 或 @Preview 装饰器页面

  • 预览和审查元素

  • 多设备预览

2)Local Emulator 本地模拟器

场景:动态页面(几乎全场景,一些无法模拟的硬件功能)

  • 新建模拟器

    一路 Next ...

  • 启动模拟器

  • 运行项目看效果

  • 更改后每次需要(打包 → 卸载 → 安装 → 预览),有没有热更新或者刷新这种预览方式?

    调试静态页面可以,一旦有了数据变化也需要重启,体验一般~ (学习需要耐心,期待 Next 做的更好~)

其他方式
  • Remote Emulator 远程模拟器
  • Remote Device 远程真机
  • Local Device 本地真机

条件:

  • 需要注册华为账号, 点这 https://developer.harmonyos.com/ 然后点击登录页面,去注册吧~
  • 登录之后,需要自动生成签名信息,这个我们后面讲真机调试再给大家演示。

5. 起步-工程结构

我们在哪里写代码?

Stage 模型

了解App结构关系,等后面我们项目阶段来讲一些工程配置文件作用,现在你要知道:

  • entry 是一个 Module 应用包

  • entryability 是一个 UIAbility 包含用户界面的应用组件

  • pages 是页面

  • components 可以是组件

组件基础

1. 组件-什么是ArkTS

ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。

扩展能力如下:

  1. 基本语法
    • 定义声明式UI、自定义组件、动态扩展UI元素;
    • 提供ArkUI系统组件,提供组件事件、方法、属性;
    • 共同构成 UI 开发主体
  2. 状态管理
    • 组件状态、组件数据共享、应用数据共享、设备共享;
  3. 渲染控制
    • 条件渲染、循环渲染、数据懒加载;
声明式UI?

问题?通过一段 HTML 标签展示出对应的页面方便,还是使用 document.createElement('tag')创建标签构建页面方便?

  • 显然是 HTML , 其实 HTML 本身就是声明式的,通过描述的方式去声明 UI 界面。
  • 一些前端框架也是声明式UI,如 Vue 使用的 tempalte 模板,如 React 使用的 JSX
  • 在例如现在的 Jetpack Compose SwiftUI Flutter 等APP开发技术也是声明式。

2. 基础-组件结构

ArkTS通过装饰器 @Component@Entry 装饰 struct 关键字声明的数据结构,构成一个自定义组件。 自定义组件中提供了一个 build 函数,开发者需在该函数内以链式调用的方式进行基本的 UI 描述,UI 描述的方法请参考 UI 描述规范。

1)页面组件

1@Entry
2@Component
3struct Index {
4  // 工程默认显示 `Index` 页面组件
5  // build 是声明UI的位置
6  build() {
7    Text('页面组件')
8  }
9}

2)自定义组件

1// 定义 `Footer` 组件
2@Component
3struct Footer {
4  build() {
5    Text('自定义组件')
6  }
7}
8
9@Entry
10@Component
11struct Index {
12  build() {
13    Column(){
14      // 使用 `Footer` 组件
15      Footer()
16    }
17  }
18}

为了更好维护,自定义组件通常会新建一个文件 Footer.ets,通过模块化语法导出导入(默认|按需)使用。

components/Footer.ets
1@Component
2export default struct Footer {
3  build() {
4    Text('自定义组件')
5  }
6}
Index.ets
1import Footer from './components/Footer.ets'
2
3@Entry
4@Component
5struct Index {
6  build() {
7    Column(){
8      // 使用 `Footer` 组件
9      Footer()
10    }
11  }
12}
TIP
  • @Entry @Component 是页面组件,将来需要页面跳转就需要再定义一个页面。
  • @Component 自定义组件,一个页面组件下可以有多个自定义组件。
  • build(){} 只能有一个根元素

3. 基础-系统组件(ArkUI)

常用系统组件 Text Column Row Button TextInput 更多组件

  • Text 文本组件
  • Column 列组件,纵向排列,Flex布局主轴是Y
  • Row 行组件,横向向排列,Flex布局主轴是X
  • Button 按钮组件
  • InputText 输入框组件

实现一个简易登录界面:

1@Entry
2@Component
3struct Index {
4
5  build() {
6    Column(){
7      Row(){
8        Text('手机号')
9        TextInput()
10      }
11      Row(){
12        Text('验证码')
13        TextInput()
14      }
15      Row(){
16        Button('重置').backgroundColor('#ccc')
17        Button('登录')
18      }
19    }
20  }
21}
TIP
  • ArkUI 组件一般都是 Flex 模式,大部分布局可以由行和列组成。

4. 基础-组件状态

如何使用 @State 定义一个状态变量?

1)组件变量,不具备驱动UI更新能力。

1@Entry
2@Component
3struct Index {
4  count = 100
5
6  build() {
7    Text(this.count.toString())
8      .onClick(() => this.count++)
9  }
10}

2)状态变量,指驱动UI更新的数据,加上 @State 装饰器即可,注意:加上类型初始值

1@Entry
2@Component
3struct Index {
4
5  @State
6  count: number = 100
7
8  build() {
9    Text(this.count.toString())
10      .onClick(() => this.count++)
11  }
12}
TIP
  • 加上类型和初始值
  • 状态变量不可设置的类型有:any undefined null 与复杂类型的联合类型

其他:

  • 绑定事件在系统组件后链式使用 onXxxxx 进行绑定即可
  • 使用 @ohos.promptAction 可以进行轻提示 promptAction.showToast({ message: 'Tip' })

📕📕📕 练习案例→实现登录表单数据收集、重置、模拟提交。

1import promptAction from '@ohos.promptAction'
2@Entry
3@Component
4struct Index {
5
6  @State
7  mobile: string = ''
8  @State
9  code: string = ''
10
11  build() {
12    Column(){
13      Row(){
14        Text('手机号')
15        TextInput({ text: this.mobile })
16          .onChange((value)=>this.mobile = value)
17      }
18      Row(){
19        Text('验证码')
20        TextInput({ text: this.code })
21          .onChange((value)=>this.code = value)
22      }
23      Row(){
24        Button('重置')
25          .backgroundColor('#ccc')
26          .onClick(()=>{
27            this.mobile = ''
28            this.code = ''
29          })
30        Button('登录')
31          .onClick(()=>{
32            if (this.mobile && this.code) {
33              promptAction.showToast({ message: `${this.mobile} 登录成功` })
34            } else {
35              promptAction.showToast({ message: `请输入手机号或验证码` })
36            }
37          })
38      }
39    }
40  }
41}

样式处理

1. 样式-语法(链式&枚举)

ArkTS以声明方式组合和扩展组件来描述应用程序的UI;
同时还提供了基本的属性、事件和子组件配置方法,帮助开发者实现应用交互逻辑。

1)样式属性

  • 属性方法以 . 链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。
1@Entry
2@Component
3struct Index {
4  build() {
5    Text('演示')
6      .backgroundColor('red')
7      .fontSize(50)
8      .width('100%')
9      .height(100)
10  }
11}

2)枚举值

  • 对于系统组件,ArkUI还为其属性预定义了一些枚举类型。文档链接
1@Entry
2@Component
3struct Index {
4  build() {
5    Text('演示')
6      .fontSize(50)
7      .width('100%')
8      .height(100)
9      .backgroundColor(Color.Blue)
10      .textAlign(TextAlign.Center)
11      .fontColor(Color.White)
12  }
13}
TIP
  • 样式相关属性通过链式函数的方式进行设置
  • 如果类型是枚举的,通过枚举传入对应的值

2. 样式-单位vp和适配

知道 vp 单位,以及适配思想

1) vp 是什么?virtual pixel

  • 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位 vp;在实际宽度为1440物理像素的屏幕上,1vp 约等于 3px(物理像素)

  • 上图的意思是,使用这个单位在不同屏幕物理分辨率的实际尺寸一致(A设备1英寸,B设备1英寸)。

2)之前 vwremrpx 相对于屏幕宽度的单位,可以实现等比例适配,vp 可以吗?

1import promptAction from '@ohos.promptAction'
2
3@Entry
4@Component
5struct Index {
6  build() {
7    Text('演示')
8      .width('100%')
9      .backgroundColor('red')
10      .onAreaChange((oldArea, newArea) => {
11        promptAction.showToast({
12          // 1. onAreaChange改变尺寸后会触发
13          // 2. newArea为现在元素尺寸
14          message: newArea.width.toString()
15        })
16      })
17  }
18}

我们发现:不同的设备屏幕的宽度 vp 是不一致的,那怎么适配呢?

3)根据官方的文档,结合自己的理解,采用:伸缩布局,网格系统,栅格系统进行布局适配。

伸缩 layoutWeight(flex: number) 占剩余空间多少份,可以理解成CSS的 flex: 1

1@Entry
2@Component
3struct Index {
4  build() {
5    Row(){
6      Text('left')
7        .layoutWeight(1)
8        .backgroundColor('red')
9      Text('right')
10        .layoutWeight(2)
11        .backgroundColor('green')
12    }
13    .width('100%')
14  }
15}

等比例,设置元素宽高比 aspectRatio(ratio: number)

1@Entry
2@Component
3struct Index {
4  build() {
5    Text('left')
6      .width('50%')
7        // 宽高比例
8      .aspectRatio(1)
9      .backgroundColor('red')
10  }
11}
TIP
  • vp 是鸿蒙默认单位,和屏幕像素有关,最终表现视觉大小在任何设备一致
  • 鸿蒙一般以伸缩 layoutWeight、网格、栅格进行布局适配,如要等比例缩放可以设置高宽比 aspectRatio

📕📕📕 练习案例→实现知乎评论回复-评论区域

设计稿一般是1080px:(这里没有设计稿,提供了一些尺寸)

  • Nav
    • 左侧返回按钮24vp高宽背景颜色#f5f5f5,图标12vp尺寸颜色#848484
    • 标题18vp
  • Comment
    • 头像尺寸32vp高宽,右侧间距10vp
    • 标题15vp,颜色默认
    • 内容16vp,颜色#565656
    • 底部12vp,颜色#c3c4c5
1@Entry
2@Component
3struct Index {
4  build() {
5    Column(){
6      // 导航
7      Row(){
8        Row(){
9          Image($r('app.media.ic_public_arrow_left'))
10            .width(16)
11            .aspectRatio(1)
12            // svg 图标可以使用填充颜色
13            // .fillColor('red')
14        }
15        .width(24)
16        .aspectRatio(1)
17        .backgroundColor('#f5f5f5')
18        .borderRadius(12)
19        .justifyContent(FlexAlign.Center)
20        .margin({ left: 16 })
21
22        Text('评论回复')
23          .layoutWeight(1)
24          .textAlign(TextAlign.Center)
25          .padding({ right: 40 })
26      }
27      .height(40)
28      .border({ width: { bottom: 0.5 }, color: '#e4e4e4' })
29      // 评论
30      Row(){
31        Image($r('app.media.avatar'))
32          .width(32)
33          .aspectRatio(1)
34          .borderRadius(16)
35        Column({ space: 5 }){
36          Text('周杰伦')
37            .width('100%')
38            .fontWeight(FontWeight.Bold)
39            .fontSize(15)
40          Text('大理石能雕刻出肌肉和皮肤的质感,那个年代的工匠好牛啊')
41            .width('100%')
42          Row(){
43            Text('10-21 · IP属地北京')
44              .fontSize(12)
45              .fontColor('#c3c4c5')
46            Row({ space: 4 }){
47              Image($r('app.media.ic_public_heart'))
48                .width(14)
49                .aspectRatio(1)
50                .fillColor('#c3c4c5')
51              Text('100')
52                .fontSize(12)
53                .fontColor('#c3c4c5')
54            }
55          }
56          .width('100%')
57          .justifyContent(FlexAlign.SpaceBetween)
58        }
59        .layoutWeight(1)
60        .padding({ left: 10 })
61      }
62      .padding(15)
63      .alignItems(VerticalAlign.Top)
64    }
65  }
66}
TIP
  • 华为官方图标下载 链接

2. 样式-@Styles 复用

在开发过程中会出现大量代码在进行重复样式设置,@Styles 可以帮我们进行样式复用

  • 当前 @Styles 仅支持 通用属性通用事件
  • 支持 全局 定义和 组件内 定义,同时存在组件内覆盖全局生效。
1// 全局
2@Styles 
3function functionName() { ... }
4
5@Entry
6@Component
7sturt Index{
8  // 组件内
9  @Styles 
10  functionName() { ... }
11
12  build() {
13    Text('Text')
14      .functionName()
15  }
16}

需求:文字和按钮相同背景,点击+1

1)全局

1@Styles function sameStyle() {
2  .backgroundColor(Color.Green)
3  .onClick(() => {
4    this.count++
5  })
6}
7
8@Entry
9@Component
10struct Index {
11  @State
12  count: number = 10
13
14  build() {
15    Column() {
16      Text(this.count.toString())
17        .width(100)
18        .height(50)
19        .margin({ bottom: 10 })
20        .textAlign(TextAlign.Center)
21        .sameStyle()
22
23      Button('+1')
24        .sameStyle()
25    }
26    .height('100%')
27    .width('100%')
28    .justifyContent(FlexAlign.Center)
29  }
30}

2)组件内

1@Entry
2@Component
3struct Index {
4  @State
5  count: number = 10
6
7  // 不需要 `function` 关键字,覆盖全局
8  @Styles
9  sameStyle (){
10    .backgroundColor(Color.Pink)
11    .onClick(() => {
12      this.count += 10
13    })
14  }
15
16  build() {
17    Column() {
18      Text(this.count.toString())
19        .width(100)
20        .height(50)
21        .margin({ bottom: 10 })
22        .textAlign(TextAlign.Center)
23        .sameStyle()
24
25      Button('+1')
26        .sameStyle()
27    }
28    .height('100%')
29    .width('100%')
30    .justifyContent(FlexAlign.Center)
31  }
32}

📕📕📕 练习案例-登录表单-样式优化

1import promptAction from '@ohos.promptAction'
2@Entry
3@Component
4struct Index {
5
6  @State
7  mobile: string = ''
8  @State
9  code: string = ''
10
11
12  @Styles
13  inputStyle () {
14    .border({ width: 1, color: Color.Gray })
15    .layoutWeight(1)
16    .margin({ left: 10, bottom: 10, top: 10 })
17    .backgroundColor(Color.White)
18  }
19
20  build() {
21    Column(){
22      Row(){
23        Text('手机号')
24        TextInput({ text: this.mobile })
25          .inputStyle()
26          .onChange((value)=>this.mobile = value)
27      }
28      Row(){
29        Text('验证码')
30        TextInput({ text: this.code })
31          .inputStyle()
32          .onChange((value)=>this.code = value)
33      }
34      Row({ space: 15 }){
35        Button('重置')
36          .backgroundColor('#ccc')
37          .onClick(()=>{
38            this.mobile = ''
39            this.code = ''
40          })
41        Button('登录')
42          .onClick(()=>{
43            if (this.mobile && this.code) {
44              promptAction.showToast({ message: `${this.mobile} 登录成功` })
45            } else {
46              promptAction.showToast({ message: `请输入手机号或验证码` })
47            }
48          })
49      }
50    }
51    .padding({ left: 15, right: 15 })
52  }
53}

3. 样式-@Extends 复用

@Extend 用于扩展原生组件样式,通过传参提供更灵活的样式复用

  • 使用 @Extend 装饰器修饰的函数只能是 全局
  • 函数可以进行 传参,如果参数是状态变量,状态更新后会刷新UI
  • 且参数可以是一个函数,实现复用事件且可处理不同逻辑
1// 全局  原生组件                     参数
2//  ↓     ↓                          ↓ 
3@Extend(Text) function functionName(w: number) { 
4  .width(w)
5}

需求:把 Text 改成按钮样式,且绑定 click 事件执行不同逻辑

1import promptAction from '@ohos.promptAction'
2
3@Extend(Text) function myClick(color: string, cb: () => void) {
4  .backgroundColor(color)
5  .width(100)
6  .height(50)
7  .textAlign(TextAlign.Center)
8  .borderRadius(25)
9  .onClick(() => cb())
10}
11
12@Entry
13@Component
14struct Other {
15  @State
16  color: string = '#ccc'
17
18  build() {
19    Column({ space: 20 }) {
20      Text('Text1')
21        .myClick(this.color, () => {
22          this.color = '#069'
23        })
24      Text('Text2')
25        .myClick('green', () => {
26          promptAction.showToast({ message: '做其他事~' })
27        })
28    }
29    .width('100%')
30    .height('100%')
31    .justifyContent(FlexAlign.Center)
32  }
33}

4. 样式-多态

stateStyles() 可以依据组件的内部状态的不同,快速设置不同样式。

stateStyles 是属性方法,可以根据UI内部状态来设置样式,类似于 css 伪类,但语法不同。ArkUI 提供以下四种状态:

  • focused:获焦态。

  • normal:正常态。

  • pressed:按压态。

  • disabled:不可用态。

1import promptAction from '@ohos.promptAction'
2
3// 胶囊按钮
4@Extend(Text)
5function capsule(){
6  .height(40)
7  .borderRadius(20)
8  .backgroundColor(Color.Gray)
9  .padding({ left: 15, right: 15 })
10  .margin({ bottom: 15 })
11}
12
13@Entry
14@Component
15struct Index {
16  @State
17  disabled: boolean = false
18  @State
19  focused: boolean = false
20
21  build() {
22    Column() {
23
24      // Button TextInput 默认开启获取焦点,页面中默认第一个这样的元素获取焦点
25      // Button 比较多限制,一个是默认开启获取焦点能看,二是禁用状态下样式无法修改
26      // Button('Button').focusable(false)
27
28      Text('toggle disabled:' + this.disabled)
29        .capsule()
30        .onClick(()=>{
31          this.disabled = !this.disabled
32        })
33      Text('toggle focused:' + this.focused)
34        .capsule()
35        .onClick(()=>{
36          this.focused = !this.focused
37        })
38      Text('clickMe')
39        .capsule()
40        .enabled(!this.disabled)
41        .focusable(this.focused)
42        .onClick(() => {
43          promptAction.showToast({ message: 'click' })
44        })
45        .fontColor('#fff')
46        .stateStyles({
47          normal: {
48            .backgroundColor(Color.Blue)
49          },
50          focused: {
51            .backgroundColor(Color.Red)
52          },
53          disabled: {
54            .backgroundColor(Color.Black)
55          },
56          pressed: {
57            .backgroundColor(Color.Orange)
58          }
59        })
60    }
61  }
62}
TIP
  • 使用比较多的应该是 normal pressed 结合下的按压效果
  • enabled(true|false) 开启|禁用 focusable(true|false) 开启获取焦点能力|关闭

注意:

  • 页面初始化的时候,默认第一个能获取焦点的元素,会自动获取焦点

组件状态

1. 状态-class语法

在鸿蒙开发中关于对象状态范式采用 class 方式,后续组件传值遇到深层对象嵌套需要利用 class

1)class 创建对象和 class 类型

1class Person {
2  age: number;
3  name: string;
4
5  constructor(name: string, age: number) {
6    this.name = name
7    this.age = age
8  }
9}
10
11// 当类型使用和构当造函数使用
12const p: Person = new Person('jack', 18)
13// 当类型使用这种使用后续居多,我们使用字面量对象居多,可以省略 constructor
14const p1: Person = {
15  name: 'tom',
16  age: 20
17}

2)implements 实现 interface 接口

1interface IPerson {
2  name: string,
3  age: number,
4  say: () => void
5}
6
7// 实现 IPerson 接口,需要符合接口约定
8class Person implements IPerson{
9  age: number;
10  name: string;
11
12  constructor(name: string, age: number) {
13    this.name = name
14    this.age = age
15  }
16
17  say () {
18    promptAction.showToast({ message: this.name })
19  }
20}

3)extends 继承

1class HmPerson extends Person {
2  github: string = 'https://github.com/zhousg'
3
4  coding () {
5    promptAction.showToast({ message: this.name + ': coding' })
6  }
7}
TIP
  • 定义类 class 类名 { 属性 + 构造器 + 方法 },类可以创建对象,也可以当类型使用。

扩展:

  • class 可以实现 implements 接口,需要受到接口约束
  • class 可以继承 extends 另外一个class的属性和方法

2. 状态-复杂状态

当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。

1)对象类型状态

1// 对象模型
2class User {
3  nickname: string
4  age: number
5}
6
7@Entry
8@Component
9struct Index {
10
11  @State
12  user: User = { nickname: 'jack', age: 18 }
13
14  build() {
15    Column({ space: 20 }){
16      Text(this.user.nickname)
17      Text(this.user.age.toString())
18      Button('age++')
19        .onClick(() => {
20          this.user.age ++
21        })
22    }
23    .width('100%')
24    .height('100%')
25    .justifyContent(FlexAlign.Center)
26  }
27}

2)嵌套对象类型状态

1// 对象模型
2class User {
3  nickname: string
4  age: number
5}
6
7class UserData {
8  code: number
9  message: string
10  // 嵌套对象
11  data: User
12}
13
14@Entry
15@Component
16struct Index {
17  @State
18  res: UserData = {
19    code: 10000,
20    message: '获取用户信息成功',
21    // 嵌套对象
22    data: { nickname: 'jack', age: 18 }
23  }
24
25  build() {
26    Column({ space: 20 }) {
27      Text(this.res.data.nickname)
28      Text(this.res.data.age.toString())
29      Button('age++')
30        .onClick(() => {
31          // ❌ this.res.data.age ++
32          const user = this.res.data
33          // 替换属性,触发UI更新
34          this.res.data = { ...user, age: user.age + 1 }
35        })
36    }
37    .width('100%')
38    .height('100%')
39    .justifyContent(FlexAlign.Center)
40  }
41}

3)对象数组类型状态

1// 对象模型
2class User {
3  nickname: string
4  age: number
5}
6
7@Entry
8@Component
9struct Index {
10  @State
11  list: User[] = [
12    { nickname: 'jack', age: 18 },
13    { nickname: 'tom', age: 16 }
14  ]
15  build() {
16    Column({ space: 20 }) {
17      Text(JSON.stringify(this.list[0]))
18      Text(JSON.stringify(this.list[1]))
19      Button('age++')
20        .onClick(() => {
21          // ❌ this.list[1].age ++
22          const user = this.list[1]
23          this.list[1] = { ...user, age: user.age + 1 }
24        })
25    }
26    .width('100%')
27    .height('100%')
28    .justifyContent(FlexAlign.Center)
29  }
30}
TIP
  • 使用 class 来约束对象类型,和嵌套对象和对象数组。
  • 嵌套对象和对象数组,采用赋值的方式进行更新,可以更新UI。
  • 🚦 后期如果学习组件传值,会给大家补充另外一种深层数据变更更新UI的方式

界面渲染

1. 渲染-条件渲染

条件渲染可根据应用的不同状态,使用if、else和else if渲染对应状态下的UI内容。

  • 条件渲染,是根据状态数据进行判断展示不同UI
  • 条件渲染,是会销毁和创建组件,组件状态将不会保留

1)使用 if else 实现 loading 效果

1@Entry
2@Component
3struct Index {
4
5  @State
6  loading: boolean = false
7
8  build() {
9    Column({ space: 20 }){
10      if (this.loading) {
11        LoadingProgress()
12          .width(100)
13          .height(100)
14      } else  {
15        Text('后台数据')
16        Text('后台数据')
17        Text('后台数据')
18      }
19      Button('更新数据')
20        .onClick(() => {
21          this.loading = true
22          setTimeout(() => {
23            this.loading = false
24          }, 2000)
25        })
26    }
27    .width('100%')
28    .height('100%')
29    .justifyContent(FlexAlign.Center)
30  }
31}

2)添加渲染会销毁和重新创建组件,组件状态将不会保留。

1@Component
2struct CounterComp {
3  @State
4  count: number = 0
5
6  build() {
7    Text(this.count.toString())
8      .onClick(() => {
9        this.count ++
10      })
11  }
12}
13
14@Entry
15@Component
16struct Index {
17
18  @State
19  show: boolean = true
20
21  build() {
22    Column({ space: 20 }){
23      if (this.show) {
24        // 累计的 count 切换时候不会保留
25        CounterComp()
26      }
27      Button('toggle')
28        .onClick(() => {
29          this.show = !this.show
30        })
31    }
32    .width('100%')
33    .height('100%')
34    .justifyContent(FlexAlign.Center)
35  }
36}

2. 渲染-循环渲染

ForEach 接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用。

语法:

1ForEach(
2  // 数据源
3  arr: Array,
4  // 组件生成函数
5  itemGenerator: (item: Array, index?: number) => void,
6  // 键值生成函数
7  keyGenerator?: (item: Array, index?: number): string => string
8)

应用:

1class User {
2  id: string
3  name: string
4  age: number
5}
6
7
8@Entry
9@Component
10struct Index {
11  @State
12  userList: User[] = []
13
14  build() {
15    Scroll() {
16      Column({ space: 20 }) {
17        // 循环渲染
18        ForEach(
19          // 1. 数据源
20          this.userList,
21          // 2. 组件生成函数
22          (item: User) => {
23            // 内容
24            Text(`${item.name} 今年 ${item.age}`)
25          },
26          // 3. 键值生成函数
27          item => item.id
28        )
29        Button('加载更多')
30          .onClick(() => {
31            const arr: User[] = []
32            for (let index = 0; index < 10; index++) {
33              arr.push({ id: Math.random().toString(), name: 'jack', age: Math.ceil(Math.random() * 100) })
34            }
35            this.userList.push(...arr)
36          })
37      }
38    }
39    .width('100%')
40  }
41}
TIP

关于 keyGenerator 键生成函数的一些建议:

  • 不能缺省
  • 避免最终生成的键包含 index
  • 对象数组,建议使用对象中 id 唯一标识
  • 基本数据数字,建议转换成具有 id 唯一标识的对象,再使用