0
点赞
收藏
分享

微信扫一扫

HarmonyOS 应用开发之创建自定义组件

在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。在进行 UI 界面开发时,通常不是简单的将系统组件进行组合使用,而是需要考虑代码可复用性、业务逻辑与UI分离,后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。

自定义组件具有以下特点:

  • 可组合:允许开发者组合使用系统组件、及其属性和方法。

  • 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。

  • 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。

自定义组件的基本用法

以下示例展示了自定义组件的基本用法。

@Component
struct HelloComponent {
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Row() {
      Text(this.message)
        .onClick(() => {
          // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

HelloComponent可以在其他自定义组件中的build()函数中多次创建,实现自定义组件的重用。

class HelloComponentParam {
  message: string = ""
}

@Entry
@Component
struct ParentComponent {
  param: HelloComponentParam = {
    message: 'Hello, World!'
  }

  build() {
    Column() {
      Text('ArkUI message')
      HelloComponent(this.param);
      Divider()
      HelloComponent(this.param);
    }
  }
}

要完全理解上面的示例,需要了解自定义组件的以下概念定义,本文将在后面的小节中介绍:

  • 自定义组件的基本结构
  • 成员函数/变量
  • 自定义组件的参数规定
  • build()函数
  • 自定义组件通用样式

自定义组件的基本结构

  • struct:自定义组件基于struct实现,struct + 自定义组件名 + {…}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
  • @Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。@Component可以接受一个可选的bool类型参数。

从API version 11开始,@Component可以接受一个可选的bool类型参数。

@Component
struct MyComponent {
}

freezeWhenInactive11+

组件冻结 选项。

名称类型必填说明
freezeWhenInactivebool是否开启组件冻结。
@Component({ freezeWhenInactive: true })
struct MyComponent {
}
  • build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。

    @Component
    struct MyComponent {
      build() {
      }
    }
    
  • @Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。

从API version 10开始,@Entry可以接受一个可选的LocalStorage 的参数或者一个可选的 EntryOptions 参数。

@Entry
@Component
struct MyComponent {
}

EntryOptions10+

命名路由跳转选项。

名称类型必填说明
routeNamestring表示作为命名路由页面的名字。
storageLocalStorage页面级的UI状态存储。
useSharedStorageboolean是否使用LocalStorage.getShared()接口返回的 LocalStorage 实例对象。
@Entry({ routeName : 'myPage' })
@Component
struct MyComponent {
}
  • @Reusable:@Reusable装饰的自定义组件具备可复用能力
@Reusable
@Component
struct MyComponent {
}

成员函数/变量

自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束:

  • 自定义组件的成员函数为私有的,且不建议声明成静态函数。

自定义组件可以包含成员变量,成员变量具有以下约束:

  • 自定义组件的成员变量为私有的,且不建议声明成静态变量。

  • 自定义组件的成员变量本地初始化有些是可选的,有些是必选的。具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量。

自定义组件的参数规定

从上文的示例中,我们已经了解到,可以在build方法里创建自定义组件,在创建自定义组件的过程中,根据装饰器的规则来初始化自定义组件的参数。

@Component
struct MyComponent {
  private countDownFrom: number = 0;
  private color: Color = Color.Blue;

  build() {
  }
}

@Entry
@Component
struct ParentComponent {
  private someColor: Color = Color.Pink;

  build() {
    Column() {
      // 创建MyComponent实例,并将创建MyComponent成员变量countDownFrom初始化为10,将成员变量color初始化为this.someColor
      MyComponent({ countDownFrom: 10, color: this.someColor })
    }
  }
}

build()函数

所有声明在build()函数的语言,我们统称为UI描述,UI描述需要遵循以下规则:

  • @Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。
    @Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。

    @Entry
    @Component
    struct MyComponent {
      build() {
        // 根节点唯一且必要,必须为容器组件
        Row() {
          ChildComponent() 
        }
      }
    }
    
    @Component
    struct ChildComponent {
      build() {
        // 根节点唯一且必要,可为非容器组件
        Image('test.jpg')
      }
    }
    
  • 不允许声明本地变量,反例如下。

    build() {
      // 反例:不允许声明本地变量
      let a: number = 1;
    }
    
  • 不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用,反例如下。

    build() {
      // 反例:不允许console.info
      console.info('print debug log');
    }
    
  • 不允许创建本地的作用域,反例如下。

    build() {
      // 反例:不允许本地作用域
      {
        ...
      }
    }
    
  • 不允许调用没有用@Builder装饰的方法,允许系统组件的参数是TS方法的返回值。

    @Component
    struct ParentComponent {
      doSomeCalculations() {
      }
    
      calcTextValue(): string {
        return 'Hello World';
      }
    
      @Builder doSomeRender() {
        Text(`Hello World`)
      }
    
      build() {
        Column() {
          // 反例:不能调用没有用@Builder装饰的方法
          this.doSomeCalculations();
          // 正例:可以调用
          this.doSomeRender();
          // 正例:参数可以为调用TS方法的返回值
          Text(this.calcTextValue())
        }
      }
    }
    
  • 不允许使用switch语法,如果需要使用条件判断,请使用if。反例如下。

    build() {
      Column() {
        // 反例:不允许使用switch语法
        switch (expression) {
          case 1:
            Text('...')
            break;
          case 2:
            Image('...')
            break;
          default:
            Text('...')
            break;
        }
      }
    }
    
  • 不允许使用表达式,反例如下。

    build() {
      Column() {
        // 反例:不允许使用表达式
        (this.aVar > 10) ? Text('...') : Image('...')
      }
    }
    
  • 不允许直接改变状态变量,反例如下。

    @Component
    struct CompA {
      @State col1: Color = Color.Yellow;
      @State col2: Color = Color.Green;
      @State count: number = 1;
      build() {
        Column() {
          // 应避免直接在Text组件内改变count的值
          Text(`${this.count++}`)
            .width(50)
            .height(50)
            .fontColor(this.col1)
            .onClick(() => {
              this.col2 = Color.Red;
            })
          Button("change col1").onClick(() =>{
            this.col1 = Color.Pink;
          })
        }
        .backgroundColor(this.col2)
      }
    }
    

    在ArkUI状态管理中,状态驱动UI更新。

所以,不能在自定义组件的build()或@Builder方法里直接改变状态变量,这可能会造成循环渲染的风险。Text(‘${this.count++}’)在全量更新或最小化更新会产生不同的影响:

  • 全量更新: ArkUI可能会陷入一个无限的重渲染的循环里,因为Text组件的每一次渲染都会改变应用的状态,就会再引起下一轮渲染的开启。 当 this.col2 更改时,都会执行整个build构建函数,因此,Text(${this.count++})绑定的文本也会更改,每次重新渲染Text(${this.count++}),又会使this.count状态变量更新,导致新一轮的build执行,从而陷入无限循环。
  • 最小化更新: 当 this.col2 更改时,只有Column组件会更新,Text组件不会更改。 只当 this.col1 更改时,会去更新整个Text组件,其所有属性函数都会执行,所以会看到Text(${this.count++})自增。因为目前UI以组件为单位进行更新,如果组件上某一个属性发生改变,会更新整体的组件。所以整体的更新链路是:this.col1 = Color.Pink -> Text组件整体更新->this.count++ ->Text组件整体更新。值得注意的是,这种写法在初次渲染时会导致Text组件渲染两次,从而对性能产生影响。

build函数中更改应用状态的行为可能会比上面的示例更加隐蔽,比如:

  • 在@Builder,@Extend或@Styles方法内改变状态变量 。

  • 在计算参数时调用函数中改变应用状态变量,例如 Text(‘${this.calcLabel()}’)。

  • 对当前数组做出修改,sort()改变了数组this.arr,随后的filter方法会返回一个新的数组。

    // 反例
    @State arr : Array<...> = [ ... ];
    ForEach(this.arr.sort().filter(...), 
      item => { 
      ...
    })
    // 正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr
    ForEach(this.arr.filter(...).sort(), 
      item => { 
      ...
    })
    

自定义组件通用样式

自定义组件通过“.”链式调用的形式设置通用样式。

@Component
struct MyComponent2 {
  build() {
    Button(`Hello World`)
  }
}

@Entry
@Component
struct MyComponent {
  build() {
    Row() {
      MyComponent2()
        .width(200)
        .height(300)
        .backgroundColor(Color.Red)
    }
  }
}

ArkUI给自定义组件设置样式时,相当于给MyComponent2套了一个不可见的容器组件,而这些样式是设置在容器组件上的,而非直接设置给MyComponent2的Button组件。通过渲染结果我们可以很清楚的看到,背景颜色红色并没有直接生效在Button上,而是生效在Button所处的开发者不可见的容器组件上。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

举报

相关推荐

0 条评论