创建自定义组件

说明:

当前为Beta阶段。

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

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

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

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

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

自定义组件的基本用法

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

@Component
class HelloComponent {
    @State
    var message: String = "Hello, World!"
    func build() {
        // HelloComponent自定义组件组合系统组件Row和Text
        Row() {
            Text(this.message)
                // 状态变量message的改变驱动UI刷新,UI从"Hello, World!"刷新为"Hello, Cangjie!"
                .onClick({etv => this.message = "Hello, Cangjie!"})
        }
    }
}

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

@Entry
@Component
class EntryView {
    func build() {
        Column() {
            Text("ArkUI message")
            HelloComponent(message: "Hello, World!")
            Divider()
            HelloComponent(message: "你好,世界!")
        }
    }
}

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

  • 自定义组件的基本结构

  • 成员函数/变量

  • 自定义组件的参数规定

  • build()函数

  • 自定义组件通用样式

自定义组件的基本结构

class

自定义组件基于class实现,class + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。

说明:

自定义组件名、类名、函数名不能和系统组件名相同。

@Component

@Component宏修饰的class为自定义组件,可以使用状态管理宏的能力。

@Component
class MyComponent {}

build()函数

build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。

@Component
class MyComponent {
    func build() {
    }
}

@Entry

@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。

@Entry
@Component
class MyComponent {}

EntryOptions

名称 类型 必填 说明
storage LocalStorage 页面级的UI状态存储。

@Reusable

@Reusable装饰的自定义组件具备可复用能力。详细请参见:@Reusable宏:组件复用

@Reusable
@Component
class MyComponent {}

成员函数/变量

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

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

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

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

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

自定义组件的参数规定

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

package ohos_app_cangjie_entry

import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*

@Component
class MyComponent {
    private var countDownFrom: Int64 = 0
    private var color: Color = Color.Blue
    func build() {
    }
}

@Entry
@Component
class EntryView {
    private var someColor: Color = Color.Red
    func build() {
        Column() {
            // 创建MyComponent实例,并将创建MyComponent成员变量countDownFrom初始化为10,将成员变量color初始化为this.someColor
            MyComponent(countDownFrom: 10, color: this.someColor)
        }
    }
}

下面的示例代码将父组件中的函数传递给子组件,并在子组件中调用。

package ohos_app_cangjie_entry

import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*

@Entry
@Component
class EntryView {
    @State
    var cnt: Int64 = 0
    func submit(): UInt {
        this.cnt++
        return 0
    }
    func build() {
        Column() {
            Text("${this.cnt}")
            Child(Childsubmit: this.submit)
        }
    }
}

@Component
class Child {
    let Childsubmit: () -> UInt
    func build() {
        Row() {
            Button("add")
                .width(80)
                .onClick({etv => this.Childsubmit()})
        }
    }
}

build()函数

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

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

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

    package ohos_app_cangjie_entry
    
    import kit.ArkUI.*
    import ohos.arkui.state_macro_manage.*
    import kit.LocalizationKit.*
    import ohos.resource.__GenerateResource__
    
    @Entry
    @Component
    class EntryView {
        func build() {
            // 根节点唯一且必要,必须为容器组件
            Row() {
                ChildComponent()
            }
        }
    }
    
    @Component
    class ChildComponent {
        func build() {
            // 根节点唯一且必要,可为非容器组件
            Image(@r(app.media.startIcon))
        }
    }
    
  • 不允许声明本地变量,反例如下。

    func build() {
        let num: Int64 = 0
    }
    
  • 不允许在UI描述里直接使用Hilog.info,但允许在方法或者函数里使用,反例如下。

    func build() {
        //反例:不允许Hilog.info
        Hilog.info(0, "HilogCj", "print debug log")
    }
    
  • 不允许创建本地的作用域,反例如下。

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

    @Component
    class EntryView {
        func doSomeCalculations() {
        }
        func calcTextValue(): String {
            return "Hello World"
        }
        @Builder
        func doSomeRender() {
            Text("Hello World")
        }
        func build() {
            Column() {
                // 反例:不能调用没有用@Builder装饰的方法
                this.doSomeCalculations()
                // 正例:可以调用
                this.doSomeRender()
                // 正例:参数可以为调用CJ方法的返回值
                Text(this.calcTextValue())
            }
        }
    }
    
  • 不允许使用match语法,如果需要使用条件判断,请使用if。示例如下。

    func build() {
        Column() {
            // 反例:不允许使用match语法
            match (expression) {
                case 0 => Text("...")
                case 1 => Text("...")
                case _ => Text("...")
            }
            // 正例:使用if
            if (expression == 1) {
                Text("...")
            } else if (expression == 2) {
                Button("...")
            } else {
                Text("...")
            }
        }
    }
    
  • 不允许直接改变状态变量,反例如下。详细分析见@State常见问题:不允许在build里改状态变量

    @Component
    class EntryView {
        @State
        var textColor: Color = Color(0xFFFF00)
        @State
        var columnColor: Color = Color.Green
        @State
        var count: Int64 = 1
        func build() {
            Column() {
                // 不允许直接在Text组件内改变count的值
                Text("${this.count++}")
                    .width(50)
                    .height(50)
                    .fontColor(this.textColor)
                    .onClick({etv => this.columnColor = Color.Red})
                Button("change textColor").onClick({etv => this.textColor = Color.Blue})
            }.backgroundColor(this.columnColor)
        }
    }
    

自定义组件通用样式

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

@Component
class ChildComponent {
    func build() {
        Button("Hello World")
    }
}

@Entry
@Component
class MyComponent {
    func build() {
        Row() {
            ChildComponent()
                .width(200)
                .height(300)
                .backgroundColor(Color.Red)
        }
    }
}

说明:

ArkUI给自定义组件设置样式时,相当于给ChildComponent套了一个不可见的容器组件,这些样式是设置在容器组件上,而非直接设置给ChildComponent的Button组件。渲染结果显示,背景颜色红色并没有直接设置到Button上,而是设置在Button所在的不可见容器组件上。